- commit
- e70e8b2
- parent
- efd35ac
- author
- x1f9
- date
- 2026-04-02 17:32:40 -0400 EDT
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
1@@ -695,6 +695,29 @@ const Daemon = struct {
2
3 log_system.deinit();
4
5+ // Redirect stdin/stdout/stderr to /dev/null. The daemon
6+ // communicates via its unix socket, not stdio. Without
7+ // this, any pipe on FDs 0-2 (e.g. from bats' `run`
8+ // keyword) stays open for the daemon's lifetime, causing
9+ // the caller to hang waiting for EOF.
10+ {
11+ const devnull = std.posix.open(
12+ "/dev/null",
13+ .{ .ACCMODE = .RDWR },
14+ 0,
15+ ) catch |err| {
16+ std.log.warn("failed to open /dev/null: {s}", .{@errorName(err)});
17+ return err;
18+ };
19+ inline for (.{ posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO }) |fd| {
20+ _ = posix.dup2(devnull, fd) catch |err| {
21+ std.log.warn("dup2 /dev/null -> {d}: {s}", .{ fd, @errorName(err) });
22+ return err;
23+ };
24+ }
25+ if (devnull > 2) posix.close(devnull);
26+ }
27+
28 // Close file descriptors inherited from the parent that the
29 // daemon doesn't need. This prevents test harnesses (like
30 // bats) from hanging — they wait for their internal FDs (3+)