Skip to content

Commit 2d3b7a6

Browse files
authored
fix(linux): cancel the X11 event loop on unregister (#38)
Previously the X11 backend blocked in XNextEvent until the hotkey was pressed, so Unregister() could not interrupt a waiting event loop: the hotkey was only released after it was next triggered, and Keydown could block forever (#15, #33). Create an invisible InputOnly window per hotkey and, on unregister, send it a custom ClientMessage (golangdesign_hotkey_cancel_hotkey) that breaks waitHotkey out of XNextEvent so the loop observes the canceled context and returns. Adds TestHotkey_Unregister. Adapted from #16 by Niklas Loeser (@data-niklas). Fixes #15.
1 parent f3e9351 commit 2d3b7a6

3 files changed

Lines changed: 140 additions & 50 deletions

File tree

hotkey_linux.c

Lines changed: 96 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,26 @@
66

77
//go:build linux
88

9-
#include <stdint.h>
10-
#include <stdio.h>
119
#include <X11/Xlib.h>
1210
#include <X11/Xutil.h>
11+
#include <stdint.h>
12+
#include <string.h> // memset
1313

1414
extern void hotkeyDown(uintptr_t hkhandle);
1515
extern void hotkeyUp(uintptr_t hkhandle);
1616

1717
int displayTest() {
18-
Display* d = NULL;
19-
for (int i = 0; i < 42; i++) {
20-
d = XOpenDisplay(0);
21-
if (d == NULL) continue;
22-
break;
23-
}
24-
if (d == NULL) {
25-
return -1;
26-
}
27-
return 0;
18+
Display *d = NULL;
19+
for (int i = 0; i < 42; i++) {
20+
d = XOpenDisplay(0);
21+
if (d == NULL)
22+
continue;
23+
break;
24+
}
25+
if (d == NULL) {
26+
return -1;
27+
}
28+
return 0;
2829
}
2930

3031
// FIXME: handle bad access properly.
@@ -38,48 +39,95 @@ int displayTest() {
3839
// pErr->minor_code );
3940
// if( pErr->request_code == 33 ){ // 33 (X_GrabKey)
4041
// if( pErr->error_code == BadAccess ){
41-
// printf("ERROR: key combination already grabbed by another client.\n");
42-
// return 0;
42+
// printf("ERROR: key combination already grabbed by another
43+
// client.\n"); return 0;
4344
// }
4445
// }
4546
// return 0;
4647
// }
4748

48-
// waitHotkey blocks until the hotkey is triggered.
49-
// this function crashes the program if the hotkey already grabbed by others.
49+
Display *openDisplay() {
50+
Display *d = NULL;
51+
for (int i = 0; i < 42; i++) {
52+
d = XOpenDisplay(0);
53+
if (d == NULL)
54+
continue;
55+
break;
56+
}
57+
return d;
58+
}
59+
60+
// Creates an invisible window, which can receive ClientMessage events. On
61+
// hotkey cancel a ClientMessageEvent is generated on the window. The event is
62+
// catched and the event loop terminates. x: 0 y: 0 w: 1 h: 1 border_width: 1
63+
// depth: 0
64+
// class: InputOnly (window will not be drawn)
65+
// visual: default visual of display
66+
// no attributes will be set (0, &attr)
67+
Window createInvisWindow(Display *d) {
68+
XSetWindowAttributes attr;
69+
return XCreateWindow(d, DefaultRootWindow(d), 0, 0, 1, 1, 0, 0, InputOnly,
70+
DefaultVisual(d, 0), 0, &attr);
71+
}
72+
73+
// Sends a custom ClientMessage of type (Atom) "go_hotkey_cancel_hotkey"
74+
// Passed value 'True' of XInternAtom creates the Atom, if it does not exist yet
75+
void sendCancel(Display *d, Window window) {
76+
Atom atom = XInternAtom(d, "golangdesign_hotkey_cancel_hotkey", True);
77+
XClientMessageEvent clientEvent;
78+
memset(&clientEvent, 0, sizeof(clientEvent));
79+
clientEvent.type = ClientMessage;
80+
clientEvent.send_event = True;
81+
clientEvent.display = d;
82+
clientEvent.window = window;
83+
clientEvent.message_type = atom;
84+
clientEvent.format = 8;
85+
86+
XEvent event;
87+
event.type = ClientMessage;
88+
event.xclient = clientEvent;
89+
XSendEvent(d, window, False, 0, &event);
90+
XFlush(d);
91+
}
92+
93+
// Closes the connection and destroys the invisible 'cancel' window
94+
void cleanupConnection(Display *d, Window w) {
95+
XDestroyWindow(d, w);
96+
XCloseDisplay(d);
97+
}
98+
99+
// waitHotkey blocks until the hotkey is triggered or canceled.
50100
//
51101
// mods points to nmods modifier masks: the same hotkey is grabbed once per
52102
// mask so that it still fires while NumLock/CapsLock are active (those locks
53103
// add bits to the event state that an exact-mask grab would not match).
54-
int waitHotkey(uintptr_t hkhandle, unsigned int* mods, int nmods, int key) {
55-
Display* d = NULL;
56-
for (int i = 0; i < 42; i++) {
57-
d = XOpenDisplay(0);
58-
if (d == NULL) continue;
59-
break;
60-
}
61-
if (d == NULL) {
62-
return -1;
63-
}
64-
int keycode = XKeysymToKeycode(d, key);
65-
for (int i = 0; i < nmods; i++) {
66-
XGrabKey(d, keycode, mods[i], DefaultRootWindow(d), False, GrabModeAsync, GrabModeAsync);
67-
}
68-
XSelectInput(d, DefaultRootWindow(d), KeyPressMask);
69-
XEvent ev;
70-
while(1) {
71-
XNextEvent(d, &ev);
72-
switch(ev.type) {
73-
case KeyPress:
74-
hotkeyDown(hkhandle);
75-
continue;
76-
case KeyRelease:
77-
hotkeyUp(hkhandle);
78-
for (int i = 0; i < nmods; i++) {
79-
XUngrabKey(d, keycode, mods[i], DefaultRootWindow(d));
80-
}
81-
XCloseDisplay(d);
82-
return 0;
83-
}
84-
}
85-
}
104+
//
105+
// d is an owned display connection and w an invisible window on it; sending w
106+
// a ClientMessage (see sendCancel) breaks the loop out of XNextEvent so an
107+
// unregister can take effect without waiting for the next keypress.
108+
int waitHotkey(uintptr_t hkhandle, unsigned int *mods, int nmods, int key,
109+
Display *d, Window w) {
110+
int keycode = XKeysymToKeycode(d, key);
111+
for (int i = 0; i < nmods; i++) {
112+
XGrabKey(d, keycode, mods[i], DefaultRootWindow(d), False, GrabModeAsync,
113+
GrabModeAsync);
114+
}
115+
XSelectInput(d, DefaultRootWindow(d), KeyPressMask);
116+
XEvent ev;
117+
while (1) {
118+
XNextEvent(d, &ev);
119+
switch (ev.type) {
120+
case KeyPress:
121+
hotkeyDown(hkhandle);
122+
continue;
123+
case KeyRelease:
124+
hotkeyUp(hkhandle);
125+
for (int i = 0; i < nmods; i++) {
126+
XUngrabKey(d, keycode, mods[i], DefaultRootWindow(d));
127+
}
128+
return 0;
129+
case ClientMessage:
130+
return 0;
131+
}
132+
}
133+
}

hotkey_linux.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ package hotkey
1212
#cgo LDFLAGS: -lX11
1313
1414
#include <stdint.h>
15+
#include <X11/Xlib.h>
1516
1617
int displayTest();
17-
int waitHotkey(uintptr_t hkhandle, unsigned int* mods, int nmods, int key);
18+
Display *openDisplay();
19+
Window createInvisWindow(Display *d);
20+
void sendCancel(Display *d, Window window);
21+
void cleanupConnection(Display *d, Window window);
22+
int waitHotkey(uintptr_t hkhandle, unsigned int* mods, int nmods, int key, Display *d, Window w);
1823
*/
1924
import "C"
2025
import (
@@ -50,6 +55,8 @@ type platformHotkey struct {
5055
ctx context.Context
5156
cancel context.CancelFunc
5257
canceled chan struct{}
58+
display *C.Display
59+
window C.Window
5360
}
5461

5562
// Nothing needs to do for register
@@ -76,6 +83,9 @@ func (hk *Hotkey) unregister() error {
7683
return errors.New("hotkey is not registered.")
7784
}
7885
hk.cancel()
86+
if hk.display != nil {
87+
C.sendCancel(hk.display, hk.window)
88+
}
7989
hk.registered = false
8090
<-hk.canceled
8191
return nil
@@ -88,12 +98,16 @@ func (hk *Hotkey) handle() {
8898
defer runtime.UnlockOSThread()
8999
// KNOWN ISSUE: if a hotkey is grabbed by others, C side will crash the program
90100

101+
hk.display = C.openDisplay()
102+
hk.window = C.createInvisWindow(hk.display)
103+
91104
var mod Modifier
92105
for _, m := range hk.mods {
93106
mod = mod | m
94107
}
95108
h := cgo.NewHandle(hk)
96109
defer h.Delete()
110+
defer hk.cleanConnection()
97111

98112
// Grab the hotkey once per NumLock/CapsLock state so it fires
99113
// regardless of those locks (see lockVariants).
@@ -109,11 +123,16 @@ func (hk *Hotkey) handle() {
109123
close(hk.canceled)
110124
return
111125
default:
112-
_ = C.waitHotkey(C.uintptr_t(h), &cmods[0], C.int(len(cmods)), C.int(hk.key))
126+
_ = C.waitHotkey(C.uintptr_t(h), &cmods[0], C.int(len(cmods)), C.int(hk.key), hk.display, hk.window)
113127
}
114128
}
115129
}
116130

131+
func (hk *Hotkey) cleanConnection() {
132+
C.cleanupConnection(hk.display, hk.window)
133+
hk.display = nil
134+
}
135+
117136
// X11 lock modifier masks (see /usr/include/X11/X.h).
118137
const (
119138
x11LockMask Modifier = 1 << 1 // CapsLock

hotkey_linux_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,26 @@ func TestHotkey(t *testing.T) {
5959
}
6060
}
6161
}
62+
63+
func TestHotkey_Unregister(t *testing.T) {
64+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
65+
defer cancel()
66+
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.Mod2, hotkey.Mod4}, hotkey.KeyA)
67+
if err := hk.Register(); err != nil {
68+
t.Errorf("failed to register hotkey: %v", err)
69+
return
70+
}
71+
if err := hk.Unregister(); err != nil {
72+
t.Errorf("failed to unregister hotkey: %v", err)
73+
return
74+
}
75+
76+
for {
77+
select {
78+
case <-ctx.Done():
79+
return
80+
case <-hk.Keydown():
81+
t.Fatalf("hotkey should not be registered but actually triggered.")
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)