From df0da389a85331f4bc021cf3f4e4b715eca5eb8c Mon Sep 17 00:00:00 2001 From: Niclas Overby Date: Tue, 28 Jul 2026 12:11:27 +0200 Subject: [PATCH] fix(launcher): detach the daemon's stdio so one-shot `darling ` exits cleanly (task #66) The launcher reuses a PERSISTENT container (container_joinable + join), but spawn_init_process forked+execv'd the daemon with no stdio redirect -- so the persistent daemon (and the shellspawn init it spawns) inherited and PINNED the caller's fd-0/1/2. A one-shot `darling ` therefore printed its output but never returned: the caller's stdout pipe stayed open (held by the persistent daemon) and daemons accumulated (leaked). In the async-signal-safe child, redirect fd-0 <- /dev/null and fd-1/2 -> $prefix/ darlingserver.log before execv (CStrings pre-built before fork). Per-command guest output flows via explicit shellspawn fd-passing, not inheritance, so this is safe; the daemon's readiness sync uses its own high fd (pipefd[1]). Validated against the green .#default install in a CLEAN runtime: `darling shell sh -c 'uname -sm'` prints BOOT=Darwin x86_64 in ~2s, the launcher EXITS CLEANLY (no hang), darlingserver.log is created, and the persistent container is preserved for reuse. (An earlier test appeared to fail only because a stale leaked container from prior testing was being joined instead of spawning fresh.) Fixes the teardown-gap half of #66; the concurrent-output flake (lost stdout + SIGPIPE under CONCURRENT fork/exec load, blocks heavy builds/M1) is separate and still open. Signed-off-by: Niclas Overby --- linux/launcher/src/main.rs | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/linux/launcher/src/main.rs b/linux/launcher/src/main.rs index ec1658aa2..278d11179 100644 --- a/linux/launcher/src/main.rs +++ b/linux/launcher/src/main.rs @@ -459,6 +459,15 @@ fn spawn_init_process(ctx: &Ctx) -> i32 { let gid_c = cstr(&ctx.orig_gid.to_string()); let pipe_c = cstr(&pipefd[1].to_string()); let fixperm_c = cstr(if ctx.fix_permissions { "1" } else { "0" }); + // Pre-built (the child is async-signal-safe, no alloc): detach the daemon's stdio. + // The daemon -- and the shellspawn init it spawns -- are PERSISTENT (the launcher + // reuses a joinable container), so if they inherit the launcher's fd-0/1/2 they pin + // the CALLER's stdout open forever and a one-shot `darling ` never sees its pipe + // close (looks like a hang; the launcher can't exit). Per-command guest output flows + // via explicit shellspawn fd-passing, not inheritance, so redirecting the daemon's + // own stdio is safe. Validated: the launcher now exits cleanly after a one-shot cmd. + let devnull_c = cstr("/dev/null"); + let log_c = cstr(&format!("{}/darlingserver.log", ctx.prefix)); let argv: [*const c_char; 7] = [ argv0.as_ptr(), prefix_c.as_ptr(), @@ -475,9 +484,31 @@ fn spawn_init_process(ctx: &Ctx) -> i32 { die("fork() failed"); } if pid == 0 { - // CHILD: async-signal-safe only -- close, execv, _exit. No allocation. + // CHILD: async-signal-safe only -- close, open/dup2, execv, _exit. No allocation. unsafe { libc::close(read_fd); + // stdin <- /dev/null, stdout+stderr -> prefix/darlingserver.log, so the + // persistent daemon/shellspawn release the caller's fd-0/1/2 (fixes the + // one-shot teardown hang). Readiness sync uses its own high fd (pipefd[1]). + let nfd = libc::open(devnull_c.as_ptr(), libc::O_RDONLY); + if nfd >= 0 { + libc::dup2(nfd, 0); + if nfd > 2 { + libc::close(nfd); + } + } + let lfd = libc::open( + log_c.as_ptr(), + libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND, + 0o644 as libc::c_int, + ); + if lfd >= 0 { + libc::dup2(lfd, 1); + libc::dup2(lfd, 2); + if lfd > 2 { + libc::close(lfd); + } + } libc::execv(ds_bin_c.as_ptr(), argv.as_ptr()); libc::_exit(1); } -- 2.51.2