Skip to content

Commit

Permalink
Implement graceful shutdown of keyboard listener threads
Browse files Browse the repository at this point in the history
  • Loading branch information
adamws committed Nov 26, 2024
1 parent 59c09bf commit ddbd435
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 33 deletions.
46 changes: 33 additions & 13 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -485,10 +485,31 @@ fn updateWindowPos(x: i32, y: i32) void {
}
}

var exit_window = false;

fn sigtermHandler(sig: c_int) callconv(.C) void {
_ = sig;
exit_window = true;
}

fn sigtermInstall() !void {
const act = std.os.linux.Sigaction{
.handler = .{ .handler = sigtermHandler },
.mask = std.os.linux.empty_sigset,
.flags = 0,
};

if (std.os.linux.sigaction(std.os.linux.SIG.TERM, &act, null) != 0) {
return error.SignalHandlerError;
}
}

pub fn main() !void {
const trace_ = tracy.trace(@src());
defer trace_.end();

try sigtermInstall();

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
Expand Down Expand Up @@ -646,9 +667,16 @@ pub fn main() !void {
key_data_producer = KeyDataProducer.init(reader.any());
}

if (res.args.replay == null) {
// TODO: defer when close supported, join on this thread won't work now
_ = try std.Thread.spawn(.{}, backend.listener, .{ &app_state, window_handle });
const replay: bool = res.args.replay != null;
const listener = switch(replay) {
false => try std.Thread.spawn(.{}, backend.listener, .{ &app_state, window_handle }),
true => null,
};
defer {
if (listener) |l| {
backend.stop();
l.join();
}
}

var keycap_texture = blk: {
Expand Down Expand Up @@ -698,7 +726,6 @@ pub fn main() !void {
// TODO: font should be configurable
const font = rl.loadFontFromMemory(".ttf", font_data, typing_font_size, codepoints);

var exit_window = false;
var show_gui = false;

const typing_persistance_sec = 2;
Expand Down Expand Up @@ -807,7 +834,7 @@ pub fn main() !void {
if (getDataForFrame(frame)) |data| {
if (data) |value| {
if (value.string[0] != 0) {
// update only for keys which produe output,
// update only for keys which produce output,
// this will not include modifiers
app_state.last_char_timestamp = std.time.timestamp();
}
Expand Down Expand Up @@ -939,15 +966,8 @@ pub fn main() !void {
}
}

// NOTE: not able to stop x11Listener yet, applicable only for x11Producer
switch (builtin.target.os.tag) {
.windows => backend.stop(),
else => {},
}
backend.is_running = false;

if (renderer) |*r| try r.wait();

std.debug.print("Exit\n", .{});
std.debug.print("Main thread exit\n", .{});
}

6 changes: 2 additions & 4 deletions src/win32.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const AppState = @import("main.zig").AppState;
const KeyData = @import("main.zig").KeyData;
const labels_lookup = @import("layout_labels.zig").labels_lookup;

pub var is_running: bool = false;
var is_running: bool = false;

var thread_id: c.DWORD = undefined;
var app_state_l: *AppState = undefined;
Expand Down Expand Up @@ -75,10 +75,8 @@ fn lowLevelKeyboardProc(nCode: c.INT, wParam: c.WPARAM, lParam: c.LPARAM) callco
}

pub fn listener(app_state: *AppState, window_handle: *anyopaque) !void {
defer is_running = false; // stopping not implemented yet
is_running = true;

_ = window_handle;
is_running = true;
thread_id = c.GetCurrentThreadId();

app_state_l = app_state;
Expand Down
40 changes: 27 additions & 13 deletions src/x11.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const std = @import("std");
const fs = std.fs;
const posix = std.posix;

const x11 = @cImport({
@cInclude("X11/Xlib.h");
Expand Down Expand Up @@ -82,7 +83,7 @@ const X11InputContext = struct {
}
};

pub var is_running: bool = false;
var is_running: bool = false;

fn xiSetMask(ptr: []u8, event: usize) void {
const offset: u3 = @truncate(event);
Expand All @@ -106,10 +107,6 @@ fn selectEvents(display: ?*x11.Display, win: x11.Window) void {
}

pub fn listener(app_state: *AppState, window_handle: *anyopaque) !void {
defer {
std.debug.print("defer x11Listener\n", .{});
is_running = false;
}
is_running = true;

const app_window = glfw.getX11Window(@ptrCast(window_handle));
Expand Down Expand Up @@ -139,13 +136,24 @@ pub fn listener(app_state: *AppState, window_handle: *anyopaque) !void {
var input_ctx = try X11InputContext.init(display, app_window);
defer input_ctx.deinit();

while (true) {
// x11 wait for event (only key press/release selected)
var ev: x11.XEvent = undefined;
const cookie: *x11.XGenericEventCookie = @ptrCast(&ev.xcookie);
// blocks, makes this thread impossible to exit:
// TODO: maybe use alarms?
// https://nrk.neocities.org/articles/x11-timeout-with-xsyncalarm
var fd = [1]posix.pollfd{posix.pollfd{
.fd = x11.ConnectionNumber(display),
.events = posix.POLL.IN,
.revents = undefined,
}};

var ev: x11.XEvent = undefined;
const cookie: *x11.XGenericEventCookie = @ptrCast(&ev.xcookie);

while (is_running) {
// Use poll to give this thread a chance for clean exit
const pending = x11.XPending(display) > 0 or try posix.poll(&fd, 100) > 0;
if (!pending) continue;

// Wait for event (only key press/release selected), should not block since
// we got here after poll but there is no documented guarantee.
// This relies on implementation details of x11, see [1] for problem description.
// [1] https://nrk.neocities.org/articles/x11-timeout-with-xsyncalarm
_ = x11.XNextEvent(display, &ev);

if (x11.XGetEventData(display, cookie) != 0 and
Expand All @@ -167,7 +175,7 @@ pub fn listener(app_state: *AppState, window_handle: *anyopaque) !void {
_ = input_ctx.lookupString(device_event, &key);

if (key.string[0] != 0) {
// update only for keys which produe output,
// update only for keys which produce output,
// this will not include modifiers
app_state.last_char_timestamp = std.time.timestamp();
}
Expand All @@ -191,6 +199,12 @@ pub fn listener(app_state: *AppState, window_handle: *anyopaque) !void {

_ = x11.XSync(display, 0);
_ = x11.XCloseDisplay(display);

std.debug.print("Exit x11 listener\n", .{});
}

pub fn stop() void {
is_running = false;
}

pub fn keysymToString(keysym: c_ulong) [*c]const u8 {
Expand Down
1 change: 1 addition & 0 deletions tests/dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pytest-html==4.1.1
pytest-xdist==3.6.1
PyVirtualDisplay==3.0
ahk==1.8.0; platform_system == "Windows"
pywin32==308; platform_system == "Windows"
17 changes: 14 additions & 3 deletions tests/src/test_rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from pyvirtualdisplay.smartdisplay import SmartDisplay

if sys.platform == "win32":
import win32con
import win32gui
from ahk import AHK
ahk = AHK()

Expand Down Expand Up @@ -210,9 +212,18 @@ def test_record_and_render(app_isolation, text: str, example) -> None:

process = processes.get("klawa")
if process and process.poll() is None:
os.kill(process.pid, signal.SIGTERM)

thread.join()
if sys.platform == "win32":
h = win32gui.FindWindow(None, "klawa")
win32gui.PostMessage(h, win32con.WM_CLOSE, 0, 0)
else:
os.kill(process.pid, signal.SIGTERM)

thread.join(timeout=2)

if thread.is_alive():
print("The klawa process is still running after the timeout")
if process and process.poll() is None:
os.kill(process.pid, signal.SIGKILL)

args = [app, "--replay", "events.bin", "--render", "output.webm"]
run_process_capture_logs(args, app_dir)
Expand Down

0 comments on commit ddbd435

Please sign in to comment.