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 { | |
| 695 | 695 | ||
| 696 | 696 | log_system.deinit(); | |
| 697 | 697 | ||
| 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 | + | ||
| 698 | 721 | // Close file descriptors inherited from the parent that the | |
| 699 | 722 | // daemon doesn't need. This prevents test harnesses (like | |
| 700 | 723 | // bats) from hanging — they wait for their internal FDs (3+) |