1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//! `astrid restart` — graceful daemon restart.
//!
//! Sends `Shutdown` to the running daemon (same path as `astrid stop`),
//! waits for the socket to close, then spawns a new persistent daemon
//! (same path as `astrid start`). Operators expect the equivalent of
//! `kill -HUP` for picking up new capsule installs.
use std::process::ExitCode;
use std::time::{Duration, Instant};
use anyhow::Result;
use tokio::time::sleep;
use crate::commands::{daemon, daemon_control};
use crate::socket_client;
use crate::theme::Theme;
/// Entry point for `astrid restart`.
pub(crate) async fn run() -> Result<ExitCode> {
daemon::handle_stop().await?;
// Wait until the socket file is gone — `handle_stop` returns as
// soon as the daemon acknowledges the request, but the actual
// cleanup races with that ack. We poll for the socket so the
// following `start` doesn't see a stale path.
let deadline = Instant::now()
.checked_add(Duration::from_secs(5))
.unwrap_or_else(Instant::now);
let socket = socket_client::proxy_socket_path();
while socket.exists() && Instant::now() < deadline {
sleep(Duration::from_millis(100)).await;
}
// Best-effort cleanup if the daemon was wedged on shutdown.
if socket.exists() {
let _ = std::fs::remove_file(&socket);
let _ = std::fs::remove_file(socket_client::readiness_path());
eprintln!(
"{}",
Theme::warning(
"Daemon did not exit cleanly within 5s — cleaning up stale socket and restarting."
)
);
}
// The socket file being gone does NOT prove the daemon process exited and
// released the singleton/state-db lock — a wedged daemon `handle_stop`
// could not reach over the socket may still be alive. Spawning now would
// race the new daemon against a held lock and fail with "Database … is
// already locked". Verify the recorded PID is actually gone (signalling it
// if it survived `handle_stop`'s own kill path) before spawning.
let pid_path = socket_client::pid_path();
if let Some((pid, _exe)) = daemon_control::read_pid_file(&pid_path)
&& daemon_control::is_process_alive(pid)
{
eprintln!(
"{}",
Theme::warning(&format!(
"A process holding the recorded daemon PID {pid} is still alive after stop — \
verifying before restart."
))
);
match daemon_control::terminate_orphan(&pid_path).await {
daemon_control::KillOutcome::StillAlive => {
anyhow::bail!(
"The previous Astrid daemon (PID {pid}) did not exit even after SIGKILL; \
refusing to start a second daemon while the lock may still be held."
);
},
daemon_control::KillOutcome::Unverified(p) => {
// Fail-secure: a live PID we can't confirm is the daemon (likely
// PID reuse) is NOT killed. Don't blindly spawn either — if it
// *were* the daemon, the lock is still held and the spawn would
// fail on it anyway. Make the operator resolve it explicitly.
anyhow::bail!(
"Cannot confirm PID {p} is the Astrid daemon (possible PID reuse); \
refusing to auto-restart. Inspect PID {p} — if it is not Astrid, \
remove {} and retry.",
pid_path.display()
);
},
_ => {
let _ = std::fs::remove_file(&pid_path);
},
}
}
daemon::spawn_persistent_daemon().await?;
Ok(ExitCode::SUCCESS)
}