- commit
- efd35ac
- parent
- a74702a
- author
- ikma
- date
- 2026-03-28 02:09:14 -0400 EDT
fix(daemon): close inherited file descriptors after fork After the daemon forks and re-initializes its own log file, close all file descriptors from 3 to 63 (excluding server_sock_fd). These FDs are inherited from the parent process and are not needed by the daemon. Without this fix, test harnesses like bats hang indefinitely. Bats uses FDs 3+ internally and waits for them to close before exiting. Since zmx forks a long-lived daemon that inherits these FDs, bats never sees them close and blocks forever. The close happens after log_system.deinit()/init() to avoid closing the parent's log FD before it's properly released, and before spawnPty() so the PTY FD (allocated after this point) is not affected. Uses std.c.close() (raw libc) instead of std.posix.close() to avoid panicking on already-closed or invalid FDs.
1 files changed,
+21,
-0
+21,
-0
1@@ -694,6 +694,27 @@ const Daemon = struct {
2 _ = try posix.setsid();
3
4 log_system.deinit();
5+
6+ // Close file descriptors inherited from the parent that the
7+ // daemon doesn't need. This prevents test harnesses (like
8+ // bats) from hanging — they wait for their internal FDs (3+)
9+ // to close before exiting.
10+ //
11+ // Must run BEFORE log_system.init() — otherwise the new log
12+ // FD gets closed, and spawnPty() reuses that FD number for
13+ // the PTY master, causing log writes to leak into the terminal.
14+ //
15+ // Skip server_sock_fd (needed for IPC) and dir.fd (needed to
16+ // delete the socket file on shutdown).
17+ {
18+ const dir_fd = @as(i32, @intCast(dir.fd));
19+ var fd: i32 = 3;
20+ while (fd < 64) : (fd += 1) {
21+ if (fd == server_sock_fd or fd == dir_fd) continue;
22+ _ = std.c.close(fd);
23+ }
24+ }
25+
26 const session_log_name = try std.fmt.allocPrint(
27 self.alloc,
28 "{s}.log",