Commit e70e8b2

x1f9  ·  2026-04-02 17:32:40 -0400 EDT
parent efd35ac
fix(daemon): redirect stdio to /dev/null after fork

The existing FD close loop (3-63) handles bats' internal file
descriptors, but bats' `run` keyword captures output via pipes
on FDs 0-2. The daemon inherits these pipe write ends and holds
them open, so bats never gets EOF and hangs.

Redirect stdin/stdout/stderr to /dev/null right after setsid().
The daemon communicates exclusively via its unix socket — it never
reads stdin or writes stdout/stderr. This is standard daemon
hygiene and completes the inherited-FD fix.
1 files changed,  +23, -0
+23, -0
......@@ -695,6 +695,29 @@ const Daemon = struct {
695695
696696 log_system.deinit();
697697
698+ // Redirect stdin/stdout/stderr to /dev/null. The daemon
699+ // communicates via its unix socket, not stdio. Without
700+ // this, any pipe on FDs 0-2 (e.g. from bats' `run`
701+ // keyword) stays open for the daemon's lifetime, causing
702+ // the caller to hang waiting for EOF.
703+ {
704+ const devnull = std.posix.open(
705+ "/dev/null",
706+ .{ .ACCMODE = .RDWR },
707+ 0,
708+ ) catch |err| {
709+ std.log.warn("failed to open /dev/null: {s}", .{@errorName(err)});
710+ return err;
711+ };
712+ inline for (.{ posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO }) |fd| {
713+ _ = posix.dup2(devnull, fd) catch |err| {
714+ std.log.warn("dup2 /dev/null -> {d}: {s}", .{ fd, @errorName(err) });
715+ return err;
716+ };
717+ }
718+ if (devnull > 2) posix.close(devnull);
719+ }
720+
698721 // Close file descriptors inherited from the parent that the
699722 // daemon doesn't need. This prevents test harnesses (like
700723 // bats) from hanging — they wait for their internal FDs (3+)