repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

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
M src/main.zig
+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+)