fix(cli): server start named a pid that had already exited

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.
This commit is contained in:
l0ng-ai
2026-08-16 06:19:51 +08:00
parent bc28f0121d
commit 2ab3b7efbe
+19 -2
View File
@@ -79,9 +79,26 @@ pub fn start() -> Result<Outcome> {
}
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() }),
)
}