From 2ab3b7efbed4868a2f0895584215ac7024da90bf Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 16 Aug 2026 06:19:51 +0800 Subject: [PATCH] fix(cli): server start named a pid that had already exited MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six `tty7 server start` at once reported six different pids, five of them gone by the time they were printed, and every one of them claimed `"started": true`. The singleton admits one server and the losers exit immediately — but they all spawned a child first, and they all then see `running()` go true, because the winner is up. Each reported the child it had spawned. `pid` is the field a script keeps in order to watch or stop the server, so five of six callers were handed a dead one. Report the pid that is actually serving, read from the pidfile, and say `started: false` when this call was not the one that started it — the same answer `start` already gives when a server was up before it ran. Readable by then: the daemon writes the pidfile after `bind`, and `running()` needs a request answered, which is later still. The ordinary path is untouched: a start that wins reports its own child, because that is the serving pid. Verified by racing six starts against an isolated config dir — all six now report the one live pid, exactly one says it started it — and by a single start, which reports the pid `ps` shows for that config dir. Also checked while looking for this, and sound: a truncated, garbage, empty or wrong-schema machine.json is quarantined (`.corrupt`, then `.corrupt.N`) and the server starts clean; a killed server's pane children do not leak, since closing the pty master hangs them up. --- crates/tty7-cli/src/server.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/tty7-cli/src/server.rs b/crates/tty7-cli/src/server.rs index 1b70d3ed..c624a96f 100644 --- a/crates/tty7-cli/src/server.rs +++ b/crates/tty7-cli/src/server.rs @@ -79,9 +79,26 @@ pub fn start() -> Result { } std::thread::sleep(POLL_INTERVAL); } + // Which pid ended up serving, which is not always the child just spawned. + // Clients that all find no server race to start one; the singleton seat + // admits exactly one and the losers exit immediately — but every one of + // them then sees `running()` go true and would report its own dead child. + // Six concurrent starts reported six different pids, five already gone, + // and `pid` is a field scripts keep in order to watch or stop the server. + // + // Readable by then: the daemon writes the pidfile after `bind`, and + // `running()` needs an answered request, which is later still. + let serving = tty7_core::daemon::pidfile::read().unwrap_or(pid); + let won = serving == pid; report( - format!("started {} (pid {pid})", exe.display()), - json!({ "started": true, "pid": pid, "exe": exe.display().to_string() }), + match won { + true => format!("started {} (pid {pid})", exe.display()), + false => format!( + "{} is already serving this config dir (pid {serving}) — another start won the race", + exe.display() + ), + }, + json!({ "started": won, "running": true, "pid": serving, "exe": exe.display().to_string() }), ) }