Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XLib event loop canceling #16

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 87 additions & 42 deletions hotkey_linux.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,26 @@

//go:build linux

#include <stdint.h>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <stdint.h>
#include <string.h> // memset

extern void hotkeyDown(uintptr_t hkhandle);
extern void hotkeyUp(uintptr_t hkhandle);

int displayTest() {
Display* d = NULL;
for (int i = 0; i < 42; i++) {
d = XOpenDisplay(0);
if (d == NULL) continue;
break;
}
if (d == NULL) {
return -1;
}
return 0;
Display *d = NULL;
for (int i = 0; i < 42; i++) {
d = XOpenDisplay(0);
if (d == NULL)
continue;
break;
}
if (d == NULL) {
return -1;
}
return 0;
}

// FIXME: handle bad access properly.
Expand All @@ -38,40 +39,84 @@ int displayTest() {
// pErr->minor_code );
// if( pErr->request_code == 33 ){ // 33 (X_GrabKey)
// if( pErr->error_code == BadAccess ){
// printf("ERROR: key combination already grabbed by another client.\n");
// return 0;
// printf("ERROR: key combination already grabbed by another
// client.\n"); return 0;
// }
// }
// return 0;
// }

Display *openDisplay() {
Display *d = NULL;
for (int i = 0; i < 42; i++) {
d = XOpenDisplay(0);
if (d == NULL)
continue;
break;
}
return d;
}

// Creates an invisible window, which can receive ClientMessage events. On
// hotkey cancel a ClientMessageEvent is generated on the window. The event is
// catched and the event loop terminates. x: 0 y: 0 w: 1 h: 1 border_width: 1
// depth: 0
// class: InputOnly (window will not be drawn)
// visual: default visual of display
// no attributes will be set (0, &attr)
Window createInvisWindow(Display *d) {
XSetWindowAttributes attr;
return XCreateWindow(d, DefaultRootWindow(d), 0, 0, 1, 1, 0, 0, InputOnly,
DefaultVisual(d, 0), 0, &attr);
}

// Sends a custom ClientMessage of type (Atom) "go_hotkey_cancel_hotkey"
// Passed value 'True' of XInternAtom creates the Atom, if it does not exist yet
void sendCancel(Display *d, Window window) {
Atom atom = XInternAtom(d, "golangdesign_hotkey_cancel_hotkey", True);
XClientMessageEvent clientEvent;
memset(&clientEvent, 0, sizeof(clientEvent));
clientEvent.type = ClientMessage;
clientEvent.send_event = True;
clientEvent.display = d;
clientEvent.window = window;
clientEvent.message_type = atom;
clientEvent.format = 8;

XEvent event;
event.type = ClientMessage;
event.xclient = clientEvent;
XSendEvent(d, window, False, 0, &event);
XFlush(d);
}

// Closes the connection and destroys the invisible 'cancel' window
void cleanupConnection(Display *d, Window w) {
XDestroyWindow(d, w);
XCloseDisplay(d);
}

// waitHotkey blocks until the hotkey is triggered.
// this function crashes the program if the hotkey already grabbed by others.
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key) {
Display* d = NULL;
for (int i = 0; i < 42; i++) {
d = XOpenDisplay(0);
if (d == NULL) continue;
break;
}
if (d == NULL) {
return -1;
}
int keycode = XKeysymToKeycode(d, key);
XGrabKey(d, keycode, mod, DefaultRootWindow(d), False, GrabModeAsync, GrabModeAsync);
XSelectInput(d, DefaultRootWindow(d), KeyPressMask);
XEvent ev;
while(1) {
XNextEvent(d, &ev);
switch(ev.type) {
case KeyPress:
hotkeyDown(hkhandle);
continue;
case KeyRelease:
hotkeyUp(hkhandle);
XUngrabKey(d, keycode, mod, DefaultRootWindow(d));
XCloseDisplay(d);
return 0;
}
}
}
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key, Display *d,
Window w) {
int keycode = XKeysymToKeycode(d, key);
XGrabKey(d, keycode, mod, DefaultRootWindow(d), False, GrabModeAsync,
GrabModeAsync);
XSelectInput(d, DefaultRootWindow(d), KeyPressMask);
XEvent ev;
while (1) {
XNextEvent(d, &ev);
switch (ev.type) {
case KeyPress:
hotkeyDown(hkhandle);
continue;
case KeyRelease:
hotkeyUp(hkhandle);
XUngrabKey(d, keycode, mod, DefaultRootWindow(d));
return 0;
case ClientMessage:
return 0;
}
}
}
24 changes: 21 additions & 3 deletions hotkey_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ package hotkey
#cgo LDFLAGS: -lX11

#include <stdint.h>
#include <X11/Xlib.h>

int displayTest();
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key);
Display *openDisplay();
Window createInvisWindow(Display *d);
void sendCancel(Display *d, Window window);
void cleanupConnection(Display *d, Window window);
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key, Display *d, Window w);
*/
import "C"
import (
Expand Down Expand Up @@ -50,6 +55,8 @@ type platformHotkey struct {
ctx context.Context
cancel context.CancelFunc
canceled chan struct{}
display *C.Display
window C.Window
}

// Nothing needs to do for register
Expand All @@ -76,6 +83,9 @@ func (hk *Hotkey) unregister() error {
return errors.New("hotkey is not registered.")
}
hk.cancel()
if hk.display != nil {
C.sendCancel(hk.display, hk.window)
}
Comment on lines +86 to +88
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel confident about this approach. What if the display is allocated after this check? What if the Unregister happens on a different goroutine to Register? This will lead to leaking resources, and race conditions of hk.display.

I think we will need some sort of synchronization here to guarantee that unregister can be called only after display and window are allocated.

hk.registered = false
<-hk.canceled
return nil
Expand All @@ -88,24 +98,32 @@ func (hk *Hotkey) handle() {
defer runtime.UnlockOSThread()
// KNOWN ISSUE: if a hotkey is grabbed by others, C side will crash the program

hk.display = C.openDisplay()
hk.window = C.createInvisWindow(hk.display)

var mod Modifier
for _, m := range hk.mods {
mod = mod | m
}
h := cgo.NewHandle(hk)
defer h.Delete()

defer hk.cleanConnection()
for {
select {
case <-hk.ctx.Done():
close(hk.canceled)
return
default:
_ = C.waitHotkey(C.uintptr_t(h), C.uint(mod), C.int(hk.key))
_ = C.waitHotkey(C.uintptr_t(h), C.uint(mod), C.int(hk.key), hk.display, hk.window)
}
}
}

func (hk *Hotkey) cleanConnection() {
C.cleanupConnection(hk.display, hk.window)
hk.display = nil
}

//export hotkeyDown
func hotkeyDown(h uintptr) {
hk := cgo.Handle(h).Value().(*Hotkey)
Expand Down
23 changes: 23 additions & 0 deletions hotkey_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,26 @@ func TestHotkey(t *testing.T) {
}
}
}

func TestHotkey_Unregister(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.Mod2, hotkey.Mod4}, hotkey.KeyA)
if err := hk.Register(); err != nil {
t.Errorf("failed to register hotkey: %v", err)
return
}
if err := hk.Unregister(); err != nil {
t.Errorf("failed to unregister hotkey: %v", err)
return
}

for {
select {
case <-ctx.Done():
return
case <-hk.Keydown():
t.Fatalf("hotkey should not be registered but actually triggered.")
}
}
}