- commit
- 690487b
- parent
- 12a19ee
- author
- Ian Tay
- date
- 2026-03-08 12:54:40 -0400 EDT
fix: signal handling — ignore SIGPIPE, handle EINTR in poll - Ignore SIGPIPE once in main() (inherited across fork, covers all subcommands). Without this, writing to a closed socket delivers SIGPIPE (default disposition: terminate) before write() can return EPIPE. An abrupt client exit killed the daemon; a daemon that died between probe and send killed one-shot clients (run/kill/history). - Handle EINTR in the daemon's poll loop (clientLoop already does this). Without this, SIGTERM/SIGCHLD caused the daemon to exit with an error instead of checking the sigterm flag and shutting down gracefully. Note: SA_RESTART is intentionally NOT set on SIGTERM/SIGWINCH. On BSD/macOS (unlike Linux), poll() is restartable when SA_RESTART is set — an idle daemon would never wake from poll() to check the sigterm flag. The EINTR handling in the poll loop is the correct and sufficient fix.
1 files changed,
+23,
-1
+23,
-1
1@@ -40,6 +40,12 @@ pub fn main() !void {
2 // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking
3 const alloc = std.heap.c_allocator;
4
5+ // Every subcommand may write to a Unix-domain socket; a peer that
6+ // disappears between probe and send would otherwise kill us before
7+ // write() can return BrokenPipe. Inherited across fork, so this also
8+ // covers the daemon.
9+ ignoreSigpipe();
10+
11 var args = try std.process.argsWithAllocator(alloc);
12 defer args.deinit();
13 _ = args.skip(); // skip program name
14@@ -1068,7 +1074,10 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void {
15 };
16 defer posix.close(probe_result.fd);
17
18- try ipc.send(probe_result.fd, .Run, cmd_to_send.?);
19+ ipc.send(probe_result.fd, .Run, cmd_to_send.?) catch |err| switch (err) {
20+ error.ConnectionResetByPeer, error.BrokenPipe => return,
21+ else => return err,
22+ };
23
24 var poll_fds = [_]posix.pollfd{.{ .fd = probe_result.fd, .events = posix.POLL.IN, .revents = 0 }};
25 const poll_result = posix.poll(&poll_fds, 5000) catch return error.PollFailed;
26@@ -1301,6 +1310,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
27 }
28
29 _ = posix.poll(poll_fds.items, -1) catch |err| {
30+ if (err == error.Interrupted) continue;
31 return err;
32 };
33
34@@ -1461,6 +1471,9 @@ fn handleSigterm(_: i32, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c)
35 sigterm_received.store(true, .release);
36 }
37
38+// No SA_RESTART on these: we WANT the signal to interrupt poll() so the
39+// loop can check the flag. On BSD/macOS, SA_RESTART makes poll restartable,
40+// which would leave an idle daemon deaf to SIGTERM until other I/O wakes it.
41 fn setupSigwinchHandler() void {
42 const act: posix.Sigaction = .{
43 .handler = .{ .sigaction = handleSigwinch },
44@@ -1478,3 +1491,12 @@ fn setupSigtermHandler() void {
45 };
46 posix.sigaction(posix.SIG.TERM, &act, null);
47 }
48+
49+fn ignoreSigpipe() void {
50+ const act: posix.Sigaction = .{
51+ .handler = .{ .handler = posix.SIG.IGN },
52+ .mask = posix.sigemptyset(),
53+ .flags = 0,
54+ };
55+ posix.sigaction(posix.SIG.PIPE, &act, null);
56+}