clipboard/listener_windows.go

408 lines
8.0 KiB
Go
Raw Normal View History

2025-11-10 10:17:06 +08:00
//go:build windows
2024-03-27 11:20:59 +08:00
package clipboard
2024-03-30 15:07:20 +08:00
import (
"b612.me/win32api"
"errors"
"fmt"
2025-11-10 10:17:06 +08:00
"runtime"
"sync"
2024-03-30 15:07:20 +08:00
"sync/atomic"
2025-11-10 10:17:06 +08:00
"syscall"
2024-03-30 15:07:20 +08:00
"time"
2025-11-10 10:17:06 +08:00
"unsafe"
2024-03-30 15:07:20 +08:00
)
var stopSign chan struct{}
var isListening uint32
/*
func listenMethod2() (<-chan Clipboard, error) {
if atomic.LoadUint32(&isListening) == 1 {
return nil, errors.New("Already listening")
}
atomic.StoreUint32(&isListening, 1)
res := make(chan Clipboard, 3)
stopSign = make(chan struct{})
hWnd, err := win32api.CreateWindowEx(0,
"Message",
"B612 Clipboard Listener",
0,
0, 0, 500, 500,
0, 0, 0, nil)
if hWnd == 0 || err != nil {
return nil, fmt.Errorf("Failed to create window: %v,hWnd:%v", err, hWnd)
}
set, err := win32api.AddClipboardFormatListener(hWnd)
if !set || err != nil {
return nil, fmt.Errorf("Failed to set clipboard listener: %v", err)
}
fetcher := make(chan struct{}, 8)
go fetchListener(hWnd, fetcher)
go func() {
for {
select {
case <-stopSign:
fmt.Println("stopped")
atomic.StoreUint32(&isListening, 0)
close(res)
close(stopSign)
win32api.RemoveClipboardFormatListener(win32api.HWND(hWnd))
win32api.DestoryWindow(hWnd)
return
case <-fetcher:
cb, err := Get()
if err != nil {
fmt.Println(err)
}
if atomic.LoadUint32(&isListening) == 1 {
res <- cb
continue
}
}
}
}()
return res, nil
}
func fetchListener(hWnd win32api.HWND, res chan struct{}) {
for {
var msg win32api.MSG
_, err := win32api.GetMessage(&msg, hWnd, 0, 0)
if msg.Message == 0x0012 {
return
}
if err == nil && win32api.DWORD(msg.Message) == win32api.WM_CLIPBOARDUPDATE {
res <- struct{}{}
}
}
}
func PauseListen() error {
if atomic.LoadUint32(&isListening) == 0 {
return errors.New("Not listening")
}
atomic.StoreUint32(&isListening, 2)
return nil
}
func RecoverListen() error {
if atomic.LoadUint32(&isListening) == 0 {
return errors.New("Not Listening")
}
atomic.StoreUint32(&isListening, 1)
return nil
}
2025-11-10 10:17:06 +08:00
2024-03-30 15:07:20 +08:00
func StopListen() error {
defer func() {
if r := recover(); r != nil {
fmt.Println("StopListen panic:", r)
}
}()
if atomic.LoadUint32(&isListening) == 0 {
return nil
}
stopSign <- struct{}{}
return nil
}
2025-11-10 10:17:06 +08:00
2024-04-02 14:11:59 +08:00
func Listen(onlyMeta bool) (<-chan Clipboard, error) {
2024-03-30 15:07:20 +08:00
if atomic.LoadUint32(&isListening) != 0 {
return nil, errors.New("Already listening")
}
atomic.StoreUint32(&isListening, 1)
res := make(chan Clipboard, 3)
stopSign = make(chan struct{})
go func() {
var storeSeq win32api.DWORD
for {
select {
case <-stopSign:
atomic.StoreUint32(&isListening, 0)
close(res)
close(stopSign)
return
case <-time.After(time.Millisecond * 900):
seq, err := win32api.GetClipboardSequenceNumber()
if err != nil {
continue
}
if seq != storeSeq {
storeSeq = seq
//fmt.Println("Clipboard updated", seq, storeSeq)
if atomic.LoadUint32(&isListening) == 1 {
2024-04-02 14:11:59 +08:00
if !onlyMeta {
cb, err := Get()
if err != nil {
continue
}
if atomic.LoadUint32(&isListening) == 1 {
res <- cb
continue
}
} else {
cb, err := GetMeta()
if err != nil {
continue
}
if atomic.LoadUint32(&isListening) == 1 {
res <- cb
continue
}
2024-03-30 15:07:20 +08:00
}
}
}
}
}
}()
return res, nil
}
2025-11-10 10:17:06 +08:00
*/
const WM_USER_STOP = win32api.WM_USER + 1
var clipboardChannel chan Clipboard
var onlyMetaGlobal bool
var hwndGlobal win32api.HWND
var lastSequence win32api.DWORD
var sequenceMux sync.Mutex
func clipboardWndProc(hWnd win32api.HWND, uMsg win32api.UINT, wParam win32api.WPARAM, lParam win32api.LPARAM) win32api.LRESULT {
switch uMsg {
case 0x031D: // WM_CLIPBOARDUPDATE
// 标记需要检查剪贴板
go checkClipboard(false)
return 0
case WM_USER_STOP:
win32api.PostQuitMessage(0)
return 0
case 0x0002: // WM_DESTROY
win32api.PostQuitMessage(0)
return 0
}
return win32api.DefWindowProc(hWnd, uMsg, wParam, lParam)
}
// 检查剪贴板是否真的有变化
func checkClipboard(ignoreMessage bool) {
if atomic.LoadUint32(&isListening) != 1 {
return
}
sequenceMux.Lock()
defer sequenceMux.Unlock()
// 获取当前剪贴板序列号
var currentSeq win32api.DWORD
runInWorker(func() {
currentSeq, _ = win32api.GetClipboardSequenceNumber()
})
// 如果序列号没有变化,跳过
if !ignoreMessage && currentSeq == lastSequence {
return
}
lastSequence = currentSeq
// 尝试读取剪贴板,带重试机制
var cb Clipboard
var err error
maxRetries := 3
retryDelay := 10 * time.Millisecond
for i := 0; i < maxRetries; i++ {
runInWorker(func() {
if !onlyMetaGlobal {
cb, err = getClipboardWithRetry()
} else {
cb, err = getMetaWithRetry()
}
})
if err == nil {
break
}
// 如果剪贴板被占用,稍等再试
if i < maxRetries-1 {
time.Sleep(retryDelay)
retryDelay *= 2 // 指数退避
}
}
if err == nil && clipboardChannel != nil {
select {
case clipboardChannel <- cb:
default:
// channel满了移除最旧的
select {
case <-clipboardChannel:
clipboardChannel <- cb
default:
}
}
}
}
func handleClipboardUpdate() {
if atomic.LoadUint32(&isListening) != 1 {
return
}
var cb Clipboard
var err error
if !onlyMetaGlobal {
cb, err = Get()
} else {
cb, err = GetMeta()
}
if err == nil && clipboardChannel != nil {
select {
case clipboardChannel <- cb:
default:
// channel满了丢弃旧数据
}
}
}
func Listen(onlyMeta bool) (<-chan Clipboard, error) {
if atomic.LoadUint32(&isListening) != 0 {
return nil, errors.New("Already listening")
}
atomic.StoreUint32(&isListening, 1)
res := make(chan Clipboard, 10) // 增大缓冲区
clipboardChannel = res
onlyMetaGlobal = onlyMeta
stopSign = make(chan struct{})
// 初始化序列号
runInWorker(func() {
lastSequence, _ = win32api.GetClipboardSequenceNumber()
})
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
defer func() {
if hwndGlobal != 0 {
win32api.RemoveClipboardFormatListener(hwndGlobal)
win32api.DestroyWindow(hwndGlobal)
hwndGlobal = 0
}
atomic.StoreUint32(&isListening, 0)
close(res)
clipboardChannel = nil
}()
hInstance, err := win32api.GetModuleHandle(nil)
if err != nil {
return
}
className := fmt.Sprintf("ClipListener_%d", time.Now().UnixNano())
var wc win32api.WNDCLASSEX
wc.CbSize = win32api.UINT(unsafe.Sizeof(wc))
wc.Style = 0
wc.LpfnWndProc = syscall.NewCallback(clipboardWndProc)
wc.CbClsExtra = 0
wc.CbWndExtra = 0
wc.HInstance = hInstance
wc.HIcon = 0
wc.HCursor = 0
wc.HbrBackground = 0
wc.LpszMenuName = nil
wc.LpszClassName = syscall.StringToUTF16Ptr(className)
wc.HIconSm = 0
_, err = win32api.RegisterClassEx(&wc)
if err != nil {
return
}
hwnd, err := win32api.CreateWindowEx(
0, className, "", 0,
0, 0, 0, 0,
0, 0, hInstance, unsafe.Pointer(nil))
if err != nil {
return
}
hwndGlobal = hwnd
ok, err := win32api.AddClipboardFormatListener(hwnd)
if err != nil || !ok {
win32api.DestroyWindow(hwnd)
return
}
go func() {
ticker := time.NewTicker(5000 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-stopSign:
return
case <-ticker.C:
// 定期检查,防止遗漏
go checkClipboard(false)
}
}
}()
// 监听停止信号
go func() {
<-stopSign
if hwndGlobal != 0 {
win32api.PostMessage(hwndGlobal, WM_USER_STOP, 0, 0)
}
}()
// 消息循环
for {
var msg win32api.MSG
ret, err := win32api.GetMessage(&msg, 0, 0, 0)
if ret == 0 {
// 收到WM_QUIT消息正常退出
break
}
if ret == 0xFFFFFFFF {
// GetMessage出错
if err != nil {
fmt.Printf("GetMessage error: %v\n", err)
}
continue
}
win32api.TranslateMessage(&msg)
win32api.DispatchMessage(&msg)
}
}()
return res, nil
}
func StopListen() {
if atomic.LoadUint32(&isListening) == 1 && stopSign != nil {
close(stopSign)
}
}
// 在工作线程中执行剪贴板操作
func runInWorker(fn func()) {
done := make(chan struct{})
clipboardWorker <- func() {
fn()
close(done)
}
<-done
}