mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
fix(daemon): block SIGTERM before any thread exists, or the shutdown save never runs
`serve_sigterm` blocks SIGTERM and waits for it on a dedicated thread, so that
`store_scrollback_now` can write every pane's screen one last time on the way
out — "the periodic writer covers the deaths nobody gets to prepare for; this
covers the ones we do".
It ran too late to do that. `pthread_sigmask` blocks a signal on the *calling*
thread only, and a signal sent to a process is delivered to any one thread that
has not blocked it. The call sat after the control listener was already
serving, so several threads had SIGTERM unblocked and the kernel could hand it
to one of them, where the default disposition ends the process on the spot:
`sigwait` never returns, no screen is written, and nothing is logged.
Measured against a running daemon, sending SIGTERM directly:
before the marker printed after the last periodic snapshot was absent from
the store, and "daemon shutting down on SIGTERM" never appeared
after the marker is in the store and the line is logged
So a machine shutting down, a logout, a supervisor stopping the daemon or a
plain `kill` cost every pane up to SNAPSHOT_INTERVAL — 30 seconds — of screen
that the code was written to keep.
The fix is where the call sits, not what it does: immediately after the
registry exists and before the control listener, which is the first thing in
`run_daemon` to start a thread. A thread inherits the mask of the one that
created it, so blocking there makes the whole process deaf to SIGTERM except on
the waiter. Blocking and waiting stay in the same call, so there is never a
window where the signal is blocked with nobody to answer it.
`tty7 server stop` was never affected: it asks over the protocol
(`ClientMsg::Shutdown`) and only falls back to signals, so it took the other
`store_scrollback_now` path.
2949 tests pass.
This commit is contained in:
@@ -329,6 +329,22 @@ pub fn run_daemon() -> anyhow::Result<()> {
|
||||
|
||||
let registry = Arc::new(Registry::new());
|
||||
|
||||
// Before anything else spawns a thread, and that is the whole point of it
|
||||
// being here rather than further down with the other startup work.
|
||||
// `pthread_sigmask` blocks a signal on the *calling thread* only, and a
|
||||
// signal sent to the process goes to any one thread that has not blocked
|
||||
// it — where SIGTERM's default disposition ends the process on the spot,
|
||||
// `sigwait` never returns, and the shutdown that was supposed to write
|
||||
// every pane's screen one last time does not happen. A thread started
|
||||
// after this inherits the mask, so blocking here and nowhere else is what
|
||||
// makes the whole process deaf to it except on the waiter.
|
||||
//
|
||||
// Nothing in this function has started a thread yet: `handoff::requested`
|
||||
// and `singleton::claim` do not, and the control listener below is the
|
||||
// first that does. Keep it that way, or move this up again.
|
||||
#[cfg(unix)]
|
||||
serve_sigterm(registry.clone());
|
||||
|
||||
#[cfg(any(unix, windows))]
|
||||
{
|
||||
let mut services = control_services();
|
||||
@@ -554,9 +570,6 @@ fn run_with(registry: Arc<Registry>) -> anyhow::Result<()> {
|
||||
|
||||
crate::daemon::pidfile::write_current();
|
||||
|
||||
#[cfg(unix)]
|
||||
serve_sigterm(registry.clone());
|
||||
|
||||
if let Some(store) = crate::core::machine::observed_store() {
|
||||
registry.seed_ids_past(&store.machine());
|
||||
let probe = registry.clone();
|
||||
|
||||
Reference in New Issue
Block a user