Skip to main content

agentmux_srv/bootstrap/
shutdown.rs

1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Split out of the single-file bootstrap module unchanged (SPEC_LARGE_FILE_MODULE_ANALYSIS_2026_09_30.md §4.3).
5
6use super::*;
7
8/// Step 6: Emit AGENTMUXSRV-ESTART on stderr (exact format from cmd/server/main-server.go:617)
9/// pending_migrations reflects any migrations that failed during the in-process
10/// run above. Non-zero causes the status-bar to show a "Migration failed —
11/// restart to retry" message. Zero is the expected steady-state.
12pub fn emit_estart(ws_port: u16, web_port: u16, version: &str, build_time: &str, instance_id: &str) {
13    let pending_migrations = migrations::count_pending_migrations(&base::get_mux_data_dir());
14    eprintln!(
15        "AGENTMUXSRV-ESTART ws:127.0.0.1:{} web:127.0.0.1:{} version:{} buildtime:{} instance:{} pending_migrations:{}",
16        ws_port, web_port, version, build_time, instance_id, pending_migrations
17    );
18}
19
20/// Steps 8 & 9: spawn the stdin-watch thread (exit on EOF — matching Go's
21/// stdinReadWatch) and the SIGINT/SIGTERM handler. Both cancel the returned
22/// token, which the WAL checkpoint loop and the final server select! also
23/// watch for graceful shutdown.
24pub fn install_shutdown_handlers(watch_stdin: bool) -> tokio_util::sync::CancellationToken {
25    // 8. Spawn stdin watch thread (exit on EOF — matching Go's stdinReadWatch).
26    //    Not in headless mode: nothing owns srv's stdin there (a container's
27    //    is /dev/null), so EOF would mean "shut down now".
28    let stdin_token = tokio_util::sync::CancellationToken::new();
29    let stdin_shutdown = stdin_token.clone();
30    if watch_stdin {
31        std::thread::spawn(move || {
32            use std::io::Read;
33            let mut stdin = std::io::stdin().lock();
34            let mut buf = [0u8; 1024];
35            loop {
36                match stdin.read(&mut buf) {
37                    Ok(0) => {
38                        eprintln_lossy!("stdin closed, shutting down");
39                        stdin_shutdown.cancel();
40                        break;
41                    }
42                    Ok(_) => {}
43                    Err(e) => {
44                        eprintln_lossy!("stdin read error: {}, shutting down", e);
45                        stdin_shutdown.cancel();
46                        break;
47                    }
48                }
49            }
50        });
51    }
52
53    // 9. Spawn signal handler (SIGINT/SIGTERM → graceful shutdown)
54    let signal_token = stdin_token.clone();
55    tokio::spawn(async move {
56        let ctrl_c = tokio::signal::ctrl_c();
57        #[cfg(unix)]
58        {
59            let mut sigterm =
60                tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).unwrap();
61            tokio::select! {
62                _ = ctrl_c => {
63                    tracing::info!("received SIGINT, shutting down");
64                }
65                _ = sigterm.recv() => {
66                    tracing::info!("received SIGTERM, shutting down");
67                }
68            }
69        }
70        #[cfg(not(unix))]
71        {
72            ctrl_c.await.ok();
73            tracing::info!("received Ctrl+C, shutting down");
74        }
75        signal_token.cancel();
76    });
77
78    stdin_token
79}
80
81/// Periodic WAL checkpoint — prevents unbounded WAL file growth during
82/// long-running sessions. Runs every 30 minutes while the srv is up.
83/// busy_timeout=5000 (set at DB open) handles transient reader contention;
84/// partial truncate on contention is safe — the remainder is picked up on
85/// the next pass. (SPEC_WINDOWS_LIFECYCLE_ROBUSTNESS_2026_06_26 §4.E)
86pub fn spawn_wal_checkpoint_loop(
87    token: tokio_util::sync::CancellationToken,
88    wal_mstore: Arc<Store>,
89    wal_filestore: Arc<FileStore>,
90) {
91    tokio::spawn(async move {
92        const INTERVAL: std::time::Duration = std::time::Duration::from_secs(30 * 60);
93        loop {
94            tokio::select! {
95                _ = tokio::time::sleep(INTERVAL) => {}
96                _ = token.cancelled() => break,
97            }
98            if let Err(e) = wal_mstore.checkpoint() {
99                tracing::warn!(error = %e, "wal_checkpoint(TRUNCATE) on objects.db failed");
100            } else {
101                tracing::debug!("wal_checkpoint(TRUNCATE): objects.db ok");
102            }
103            if let Err(e) = wal_filestore.checkpoint() {
104                tracing::warn!(error = %e, "wal_checkpoint(TRUNCATE) on filestore.db failed");
105            } else {
106                tracing::debug!("wal_checkpoint(TRUNCATE): filestore.db ok");
107            }
108        }
109    });
110}