408 lines
8.0 KiB
Go
408 lines
8.0 KiB
Go
//go:build windows
|
||
|
||
package clipboard
|
||
|
||
import (
|
||
"b612.me/win32api"
|
||
"errors"
|
||
"fmt"
|
||
"runtime"
|
||
"sync"
|
||
"sync/atomic"
|
||
"syscall"
|
||
"time"
|
||
"unsafe"
|
||
)
|
||
|
||
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
|
||
}
|
||
|
||
|
||
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
|
||
}
|
||
|
||
|
||
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, 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 {
|
||
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
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
}()
|
||
return res, nil
|
||
}
|
||
*/
|
||
|
||
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
|
||
}
|