Commit 690487b

Ian Tay  ·  2026-03-08 12:54:40 -0400 EDT
parent 12a19ee
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
......@@ -40,6 +40,12 @@ pub fn main() !void {
4040 // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking
4141 const alloc = std.heap.c_allocator;
4242
43+ // Every subcommand may write to a Unix-domain socket; a peer that
44+ // disappears between probe and send would otherwise kill us before
45+ // write() can return BrokenPipe. Inherited across fork, so this also
46+ // covers the daemon.
47+ ignoreSigpipe();
48+
4349 var args = try std.process.argsWithAllocator(alloc);
4450 defer args.deinit();
4551 _ = args.skip(); // skip program name
......@@ -1068,7 +1074,10 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void {
10681074 };
10691075 defer posix.close(probe_result.fd);
10701076
1071- try ipc.send(probe_result.fd, .Run, cmd_to_send.?);
1077+ ipc.send(probe_result.fd, .Run, cmd_to_send.?) catch |err| switch (err) {
1078+ error.ConnectionResetByPeer, error.BrokenPipe => return,
1079+ else => return err,
1080+ };
10721081
10731082 var poll_fds = [_]posix.pollfd{.{ .fd = probe_result.fd, .events = posix.POLL.IN, .revents = 0 }};
10741083 const poll_result = posix.poll(&poll_fds, 5000) catch return error.PollFailed;
......@@ -1301,6 +1310,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
13011310 }
13021311
13031312 _ = posix.poll(poll_fds.items, -1) catch |err| {
1313+ if (err == error.Interrupted) continue;
13041314 return err;
13051315 };
13061316
......@@ -1461,6 +1471,9 @@ fn handleSigterm(_: i32, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c)
14611471 sigterm_received.store(true, .release);
14621472 }
14631473
1474+// No SA_RESTART on these: we WANT the signal to interrupt poll() so the
1475+// loop can check the flag. On BSD/macOS, SA_RESTART makes poll restartable,
1476+// which would leave an idle daemon deaf to SIGTERM until other I/O wakes it.
14641477 fn setupSigwinchHandler() void {
14651478 const act: posix.Sigaction = .{
14661479 .handler = .{ .sigaction = handleSigwinch },
......@@ -1478,3 +1491,12 @@ fn setupSigtermHandler() void {
14781491 };
14791492 posix.sigaction(posix.SIG.TERM, &act, null);
14801493 }
1494+
1495+fn ignoreSigpipe() void {
1496+ const act: posix.Sigaction = .{
1497+ .handler = .{ .handler = posix.SIG.IGN },
1498+ .mask = posix.sigemptyset(),
1499+ .flags = 0,
1500+ };
1501+ posix.sigaction(posix.SIG.PIPE, &act, null);
1502+}