@@ -29,6 +29,23 @@ type x11HotkeyBinding struct {
2929}
3030
3131type x11HotkeyState struct {
32+ // displayMu serializes every Xlib call made on display.
33+ //
34+ // Xlib is thread-safe only when XInitThreads runs before the first
35+ // XOpenDisplay, and nothing in this process calls it. Manager.mu guards the
36+ // Go maps and says nothing about the connection, so without this lock a
37+ // registration on a caller's goroutine and the poll loop on its own would
38+ // be inside the same connection buffers at once: XPending flushes the
39+ // output buffer and reads from the socket, which is exactly what the
40+ // XGrabKey/XSelectInput/XFlush sequence beside it is doing.
41+ //
42+ // It is never held across a call that waits for a key. The loop polls with
43+ // neru_hotkeys_pending and only calls XNextEvent when an event is already
44+ // queued, so the longest a registration can wait behind it is one
45+ // non-blocking read — holding it across a bare XNextEvent would wedge
46+ // registration until the user pressed something.
47+ displayMu sync.Mutex
48+
3249 display * C.Display
3350 root C.Window
3451 bindings map [ports.HotkeyID ]x11HotkeyBinding
@@ -38,35 +55,115 @@ type x11HotkeyState struct {
3855 once sync.Once
3956}
4057
41- var x11States sync.Map
58+ // x11IgnoredModifierMasks are the lock modifiers a grab has to be repeated
59+ // under, because the server reports them in a key event's state and a grab
60+ // naming a different state does not match: Lock is CapsLock, and Mod2 is where
61+ // NumLock conventionally sits.
62+ //
63+ // The set is stated once so a grab and the ungrab that undoes it cannot drift:
64+ // an ungrab that misses a mask leaves a hotkey grabbed after Neru forgot it.
65+ func x11IgnoredModifierMasks () [4 ]C.uint {
66+ return [4 ]C.uint {0 , C .Mod2Mask , C .LockMask , C .Mod2Mask | C .LockMask }
67+ }
4268
43- func (m * Manager ) registerX11Hotkey (hotkeyID ports.HotkeyID , keyString string ) error {
44- state , err := m .ensureX11State ()
69+ // grab resolves keyString against the connection's keymap and installs the
70+ // grabs for it, as one critical section.
71+ //
72+ // The resolve belongs inside the lock rather than beside it: XKeysymToKeycode
73+ // is an Xlib call on this same connection, so it is one of the calls being
74+ // serialized, not a pure computation that happens to precede them.
75+ func (s * x11HotkeyState ) grab (keyString string ) (x11HotkeyBinding , error ) {
76+ s .displayMu .Lock ()
77+ defer s .displayMu .Unlock ()
78+
79+ keycode , modifiers , err := parseX11Hotkey (s .display , keyString )
4580 if err != nil {
46- return err
47- }
48-
49- keycode , modifiers , parseErr := parseX11Hotkey (state .display , keyString )
50- if parseErr != nil {
51- return parseErr
81+ return x11HotkeyBinding {}, err
5282 }
5383
54- for _ , mask := range []C. uint { 0 , C . Mod2Mask , C . LockMask , C . Mod2Mask | C . LockMask } {
84+ for _ , mask := range x11IgnoredModifierMasks () {
5585 C .XGrabKey (
56- state .display ,
86+ s .display ,
5787 C .int (keycode ),
5888 modifiers | mask ,
59- state .root ,
89+ s .root ,
6090 C .True ,
6191 C .GrabModeAsync ,
6292 C .GrabModeAsync ,
6393 )
6494 }
65- C .XSelectInput (state .display , state .root , C .KeyPressMask )
66- C .XFlush (state .display )
6795
68- state .bindings [hotkeyID ] = x11HotkeyBinding {keycode : C .int (keycode ), modifiers : modifiers }
69- state .ids [x11BindingKey (keycode , modifiers )] = hotkeyID
96+ C .XSelectInput (s .display , s .root , C .KeyPressMask )
97+ C .XFlush (s .display )
98+
99+ return x11HotkeyBinding {keycode : C .int (keycode ), modifiers : modifiers }, nil
100+ }
101+
102+ // ungrab releases the grabs grab installed for one binding.
103+ func (s * x11HotkeyState ) ungrab (binding x11HotkeyBinding ) {
104+ s .displayMu .Lock ()
105+ defer s .displayMu .Unlock ()
106+
107+ for _ , mask := range x11IgnoredModifierMasks () {
108+ C .XUngrabKey (
109+ s .display ,
110+ binding .keycode ,
111+ binding .modifiers | mask ,
112+ s .root ,
113+ )
114+ }
115+
116+ C .XFlush (s .display )
117+ }
118+
119+ // nextEvent takes one queued event off the connection, reporting false when
120+ // none was waiting.
121+ //
122+ // The pending check and the read are one critical section because they are one
123+ // sequence: XPending answering "an event is queued" is only a fact about this
124+ // connection while nothing else is draining it, and an XNextEvent whose event
125+ // another thread has taken blocks until the next one arrives — with displayMu
126+ // held, which is the one way this lock could wedge registration.
127+ func (s * x11HotkeyState ) nextEvent () (C.XEvent , bool ) {
128+ s .displayMu .Lock ()
129+ defer s .displayMu .Unlock ()
130+
131+ var event C.XEvent
132+
133+ if C .neru_hotkeys_pending (s .display ) == 0 {
134+ return event , false
135+ }
136+
137+ C .XNextEvent (s .display , & event )
138+
139+ return event , true
140+ }
141+
142+ // closeDisplay tears down the connection. Callers must have stopped the poll
143+ // loop first — the lock orders this against a registration, not against a read
144+ // of a display that has been freed.
145+ func (s * x11HotkeyState ) closeDisplay () {
146+ s .displayMu .Lock ()
147+ defer s .displayMu .Unlock ()
148+
149+ C .XCloseDisplay (s .display )
150+ }
151+
152+ var x11States sync.Map
153+
154+ func (m * Manager ) registerX11Hotkey (hotkeyID ports.HotkeyID , keyString string ) error {
155+ state , err := m .ensureX11State ()
156+ if err != nil {
157+ return err
158+ }
159+
160+ binding , grabErr := state .grab (keyString )
161+ if grabErr != nil {
162+ return grabErr
163+ }
164+
165+ state .bindings [hotkeyID ] = binding
166+ state .ids [x11BindingKey (C .uint (binding .keycode ), binding .modifiers )] = hotkeyID
70167
71168 return nil
72169}
@@ -86,15 +183,7 @@ func (m *Manager) unregisterX11Hotkey(hotkeyID ports.HotkeyID) {
86183 return
87184 }
88185
89- for _ , mask := range []C.uint {0 , C .Mod2Mask , C .LockMask , C .Mod2Mask | C .LockMask } {
90- C .XUngrabKey (
91- state .display ,
92- binding .keycode ,
93- binding .modifiers | mask ,
94- state .root ,
95- )
96- }
97- C .XFlush (state .display )
186+ state .ungrab (binding )
98187
99188 delete (state .ids , x11BindingKey (C .uint (binding .keycode ), binding .modifiers ))
100189 delete (state .bindings , hotkeyID )
@@ -120,7 +209,7 @@ func (m *Manager) unregisterAllX11Hotkeys() {
120209 close (state .stopCh )
121210 <- state .doneCh
122211
123- C . XCloseDisplay ( state .display )
212+ state .closeDisplay ( )
124213 x11States .Delete (m )
125214 })
126215}
@@ -135,6 +224,10 @@ func (m *Manager) ensureX11State() (*x11HotkeyState, error) {
135224 return state , nil
136225 }
137226
227+ // XOpenDisplay and the root-window read are the two Xlib calls here that do
228+ // not go through displayMu, and the only ones that need not: they run
229+ // before the state exists to lock, before it is published to x11States and
230+ // before the poll loop that would share the connection is started.
138231 display := C .XOpenDisplay (nil )
139232 if display == nil {
140233 return nil , derrors .New (
@@ -167,18 +260,19 @@ func (m *Manager) runX11HotkeyLoop(state *x11HotkeyState) {
167260 default :
168261 }
169262
170- // Use XPending to check for queued events instead of calling the
171- // blocking XNextEvent directly. This allows the stop channel to be
172- // checked between iterations, preventing a goroutine leak and a
173- // use-after-free when unregisterAllX11Hotkeys closes the display.
174- if C .neru_hotkeys_pending (state .display ) == 0 {
263+ // nextEvent polls with XPending instead of calling the blocking
264+ // XNextEvent directly. This allows the stop channel to be checked
265+ // between iterations, preventing a goroutine leak and a
266+ // use-after-free when unregisterAllX11Hotkeys closes the display —
267+ // and it is what lets every Xlib call on this connection share one
268+ // lock without a registration ever waiting on a keystroke.
269+ event , hasEvent := state .nextEvent ()
270+ if ! hasEvent {
175271 time .Sleep (x11HotkeyPollInterval )
176272
177273 continue
178274 }
179275
180- var event C.XEvent
181- C .XNextEvent (state .display , & event )
182276 if C .neru_xevent_type (& event ) != C .KeyPress {
183277 continue
184278 }
0 commit comments