Skip to content

Commit 73730a9

Browse files
authored
fix(windows): keep a stopped keyboard hook's WM_QUIT off other message pumps (#1598)
1 parent 71d1b34 commit 73730a9

1 file changed

Lines changed: 25 additions & 11 deletions

File tree

internal/adapter/platform/windows/keyboard_hook.go

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package windows
44

55
import (
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

3436
type 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

243247
func (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

Comments
 (0)