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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//! `tm status`'s daemon line, derived from the SAME probe `tm doctor` uses
//! (#8025).
//!
//! Why: after a launchd restart on 2026-09-15 the two commands disagreed in the
//! same minute — `tm status` printed `daemon: unreachable` while `tm doctor`
//! printed `trusty_mpm_daemon trusty-mpm daemon: reachable` and named pid
//! 65875. They disagreed because they asked different questions.
//! `commands::daemon::daemon_healthy` required `/health` AND `/sessions` to
//! both answer 200, so a daemon whose session listing was slow, wedged, or
//! erroring — the exact state the same session reported minutes earlier, when
//! `…/sessions/<id>/delegations/builder-slot` "did not complete" — read as
//! UNREACHABLE. `tm doctor` asked `/health` alone and read it as reachable.
//! Neither was lying; "the fleet listing failed" is simply not the same fact as
//! "the daemon is unreachable", and collapsing them cost an operator the one
//! signal that would have told them which.
//!
//! What: [`run`] issues ONE
//! [`super::doctor_daemon_row::probe_daemon`] — doctor's probe, not a second
//! copy of it — renders the verdict through [`daemon_line`], and only then
//! fetches the session listing. A listing that fails is reported as an
//! unavailable LISTING, on its own line, with the daemon line left as the probe
//! found it.
//!
//! The pid comes from that probe's `/health` snapshot and from nowhere else.
//! `~/.trusty-mpm/daemon.lock` records the pid of whichever daemon wrote it,
//! which after a restart is not necessarily the one answering — so it is not an
//! input here, and [`daemon_line`] cannot consult it.
//!
//! Test: `src/bin/tm/commands/status_daemon_tests.rs`.
use trusty_mpm::client::HealthSnapshot;
use super::doctor_daemon_row::{DaemonReachability, probe_daemon};
/// Render the one daemon line `tm status` prints, from the probe alone.
///
/// Why: a pure function over the probe's own two outputs is what makes "status
/// and doctor cannot disagree" assertable — there is no second source of truth
/// left for it to read. It is also why the pid argument is the snapshot rather
/// than a `u32` the caller resolved: a caller holding only a pid could have got
/// it from the lock file.
/// What: `daemon: reachable (pid N, version V)` when `/health` answered;
/// `daemon: unreachable` verbatim when nothing is listening, which is the
/// string `tm status` has always printed for that case; `daemon: unresponsive`
/// plus the remedy when a socket accepted but the probe went unanswered. A
/// daemon too old to report `pid`/`version` yields `pid unknown` /
/// `version unknown` rather than a fabricated zero.
/// Test: `daemon_line_names_the_probed_pid`,
/// `daemon_line_keeps_the_historical_unreachable_wording`,
/// `daemon_line_separates_unresponsive_from_unreachable`,
/// `daemon_line_reports_an_old_daemon_pid_as_unknown`.
pub(crate) fn daemon_line(
reachability: DaemonReachability,
snapshot: Option<&HealthSnapshot>,
) -> String {
match reachability {
DaemonReachability::Reachable => {
// A pre-#4230 daemon omits `pid` entirely, which deserialises to
// `None`. Saying "unknown" is the only honest reading — and it must
// NOT be answered from the lock file, whose pid may name the daemon
// that just died rather than the one that answered.
let pid = match snapshot.and_then(|s| s.pid) {
Some(pid) => pid.to_string(),
None => "unknown".to_string(),
};
let version = snapshot
.map(|s| s.version.as_str())
.filter(|v| !v.is_empty())
.unwrap_or("unknown");
format!("daemon: reachable (pid {pid}, version {version})")
}
DaemonReachability::NotRunning => "daemon: unreachable".to_string(),
// #6288: the OS refused the dial; the daemon's own state is unknown.
DaemonReachability::Denied(errno) => format!(
"daemon: unreachable — the OS refused the dial ({})",
super::doctor_daemon_row::errno_name(errno)
),
DaemonReachability::Unresponsive => "daemon: unresponsive — it accepted the connection \
but did not answer the health probe in time; restart it and re-run `tm doctor`"
.to_string(),
}
}
/// The line printed when the daemon answered but its session listing did not.
///
/// Why: this is the case the old `daemon_healthy` conflated away. Naming the
/// listing — not the daemon — is what keeps `tm status` and `tm doctor` from
/// contradicting each other about reachability while still telling the operator
/// that something is wrong.
/// Test: `status_reports_the_live_daemon_when_the_listing_fails`.
pub(crate) fn listing_unavailable_line(err: &anyhow::Error) -> String {
format!("sessions: listing unavailable ({err:#}) — the daemon itself answered /health")
}
/// `status` subcommand — probe daemon health, then list sessions.
///
/// Why: the first thing an operator runs to see if the daemon is alive, and
/// (#8025) the answer must be the same one `tm doctor` gives.
/// What: one [`probe_daemon`], [`daemon_line`], and — only when the probe
/// reached the daemon — the session listing, whose own failure is reported
/// through [`listing_unavailable_line`] rather than as an exit code or a
/// contradictory daemon verdict.
/// #6288 step 1: over the socket, a daemon that is not reachable fails the
/// command with an error naming the socket — never exit 0.
/// Test: `src/bin/tm/commands/status_daemon_tests.rs` drives both halves;
/// `tm_status_over_an_absent_socket_fails_naming_it` the error arm.
pub(crate) async fn run(daemon: &trusty_mpm::client::DaemonClient) -> anyhow::Result<()> {
let (reachability, snapshot) = probe_daemon(daemon).await;
println!("{}", daemon_line(reachability, snapshot.as_ref()));
if reachability != DaemonReachability::Reachable {
anyhow::bail!(
"trusty-mpm daemon not reachable over {}",
daemon.transport_label()
);
}
// #8058: a daemon that has parked a subsystem answers `/health` 200 all the
// same, so the operator only learns of it if something prints it. This is
// that reader.
for reason in snapshot.iter().flat_map(|s| s.degraded.iter()) {
println!("degraded: {reason}");
}
if let Err(e) = super::daemon::print_sessions(daemon).await {
println!("{}", listing_unavailable_line(&e));
}
Ok(())
}
#[cfg(test)]
#[path = "status_daemon_tests.rs"]
mod status_daemon_tests;