@@ -4,6 +4,7 @@ package windows
44
55import (
66 "errors"
7+ "runtime"
78 "sync"
89 "sync/atomic"
910 "syscall"
@@ -29,6 +30,7 @@ const (
2930 wmQuit = 0x0012
3031 llkhfUp = 0x0080
3132 pmRemove = 0x0001
33+ pmNoRemove = 0x0000
3234)
3335
3436type kbdLLHookStruct struct {
@@ -154,8 +156,10 @@ func (h *KeyboardHook) Stop() {
154156 // WM_QUIT to the hook thread makes GetMessage return 0 and end the
155157 // loop. Without this, Stop deadlocks on doneCh (the loop's own
156158 // stopCh check only runs between GetMessage calls, which it never
157- // reaches while blocked). The in-loop check still covers the race
158- // where Stop fires before the first GetMessage.
159+ // reaches while blocked). The in-loop stopCh check still covers a Stop
160+ // that fires before the first GetMessage. The queue exists by then (run
161+ // creates it before signaling readiness), and the WM_QUIT this leaves
162+ // queued dies with the locked thread.
159163 h .mu .Lock ()
160164 threadID := h .threadID
161165 h .mu .Unlock ()
@@ -241,6 +245,16 @@ func keyboardHookProc(code int, wParam uintptr, lParam unsafe.Pointer) uintptr {
241245}
242246
243247func (h * KeyboardHook ) run () {
248+ // The hook and its message queue are thread-affine (doc.go), and the
249+ // thread is deliberately never unlocked: a goroutine that exits while
250+ // locked takes its OS thread down with it, queue included. Stop's WM_QUIT
251+ // is consumed by the GetMessage it wakes, but a Stop that lands before the
252+ // first GetMessage leaves it queued, and a thread handed back to the
253+ // scheduler with a WM_QUIT in its queue ends the next message pump that
254+ // runs on it. The display and foreground watchers lock whatever thread
255+ // they start on and would exit before their first real message.
256+ runtime .LockOSThread ()
257+
244258 defer close (h .doneCh )
245259
246260 handle , _ , _ := procSetWindowsHookExW .Call (
@@ -257,6 +271,15 @@ func (h *KeyboardHook) run() {
257271 return
258272 }
259273
274+ // A thread gets a message queue on its first call that needs one, and
275+ // PostThreadMessage to a thread without one fails. Stop posts WM_QUIT as
276+ // soon as StartKeyboardHook returns, which can be before the loop's first
277+ // GetMessage, so the queue is forced into existence here, ahead of the
278+ // readiness signal. PeekMessage with PM_NOREMOVE is the documented way.
279+ var probe msg
280+
281+ discardCall (procPeekMessageW .Call (uintptr (unsafe .Pointer (& probe )), 0 , 0 , 0 , pmNoRemove ))
282+
260283 h .mu .Lock ()
261284 h .hook = handle
262285 threadID , _ , _ := procGetCurrentThreadID .Call ()
@@ -285,15 +308,6 @@ func (h *KeyboardHook) run() {
285308 for {
286309 select {
287310 case <- h .stopCh :
288- if h .threadID != 0 {
289- _ , _ , _ = procPostThreadMessageW .Call (
290- uintptr (h .threadID ),
291- wmQuit ,
292- 0 ,
293- 0 ,
294- )
295- }
296-
297311 return
298312 default :
299313 }
0 commit comments