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 { | |
| 40 | 40 | // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking | |
| 41 | 41 | const alloc = std.heap.c_allocator; | |
| 42 | 42 | ||
| 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 | + | ||
| 43 | 49 | var args = try std.process.argsWithAllocator(alloc); | |
| 44 | 50 | defer args.deinit(); | |
| 45 | 51 | _ = args.skip(); // skip program name |
| ... | ... | @@ -1068,7 +1074,10 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void { | |
| 1068 | 1074 | }; | |
| 1069 | 1075 | defer posix.close(probe_result.fd); | |
| 1070 | 1076 | ||
| 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 | + | }; | |
| 1072 | 1081 | ||
| 1073 | 1082 | var poll_fds = [_]posix.pollfd{.{ .fd = probe_result.fd, .events = posix.POLL.IN, .revents = 0 }}; | |
| 1074 | 1083 | 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 { | |
| 1301 | 1310 | } | |
| 1302 | 1311 | ||
| 1303 | 1312 | _ = posix.poll(poll_fds.items, -1) catch |err| { | |
| 1313 | + | if (err == error.Interrupted) continue; | |
| 1304 | 1314 | return err; | |
| 1305 | 1315 | }; | |
| 1306 | 1316 |
| ... | ... | @@ -1461,6 +1471,9 @@ fn handleSigterm(_: i32, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) | |
| 1461 | 1471 | sigterm_received.store(true, .release); | |
| 1462 | 1472 | } | |
| 1463 | 1473 | ||
| 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. | |
| 1464 | 1477 | fn setupSigwinchHandler() void { | |
| 1465 | 1478 | const act: posix.Sigaction = .{ | |
| 1466 | 1479 | .handler = .{ .sigaction = handleSigwinch }, |
| ... | ... | @@ -1478,3 +1491,12 @@ fn setupSigtermHandler() void { | |
| 1478 | 1491 | }; | |
| 1479 | 1492 | posix.sigaction(posix.SIG.TERM, &act, null); | |
| 1480 | 1493 | } | |
| 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 | + | } |