onlyne_client/runtime/daemon.rs
1//! Liveness for one role workspace.
2//!
3//! The client never detaches itself: `run` is the only launch verb, and
4//! backgrounding is the operator's job — a visible terminal tab, `launchd`, or
5//! `nohup`. So no pid file is written and nothing signals a process by number.
6//! `status` asks the workspace socket instead: a client is running when its
7//! adapter socket answers the admin `hello` probe, and the registration's mtime
8//! dates that client. The socket lives in the machine-level runtime directory
9//! (`<runtime>/<digest>.sock`) beside the `<digest>.json` that names the surface
10//! serving it, so the path is short enough to probe on every platform and the
11//! `run/s` spelling is only what operators read.
12
13use anyhow::Result;
14use onlyne_config::layout::RoleWorkspace;
15use onlyne_wire::socket::registration_path;
16use std::path::{Path, PathBuf};
17use std::time::Duration;
18
19/// Byte-exact answer for every verb that needs a live client.
20pub const NOT_RUNNING: &str = "onlyne: client not running";
21/// Byte-exact answer for a live client whose server link is down.
22pub const NOT_CONNECTED: &str = "onlyne: client not connected";
23/// Event window scanned when `status` counts recorded faults.
24pub const FAULT_SCAN_LIMIT: u32 = 10_000;
25
26/// Facts `status` reports for a live client.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct StatusReport {
29 pub uptime: Duration,
30 pub socket: PathBuf,
31 pub faults: usize,
32 /// Whether the client holds a ready server link.
33 pub connected: bool,
34}
35
36impl StatusReport {
37 /// One operator line carrying every reported field.
38 pub fn line(&self) -> String {
39 format!(
40 "onlyne: client running uptime {}s socket {} faults {}",
41 self.uptime.as_secs(),
42 self.socket.display(),
43 self.faults
44 )
45 }
46
47 /// Process exit code for the `status` verb: zero for a client that is up
48 /// and connected to its server, and the refusal code otherwise.
49 pub fn exit_code(&self) -> i32 {
50 if self.connected { 0 } else { 2 }
51 }
52}
53
54/// Report the client serving `workspace`, and `None` when none is running.
55///
56/// A client is running when its adapter socket answers an `admin` `hello`: a
57/// registration no process answers is what an unclean exit leaves behind, and
58/// this verb refuses it exactly as it refuses a missing socket. The link state
59/// is the fact the answering client holds, and the uptime is the age of the
60/// registration that client published.
61pub async fn status(workspace: &Path) -> Result<Option<StatusReport>> {
62 let layout = RoleWorkspace::resolve(workspace);
63 let socket = layout.socket_path();
64 let Some(connected) = crate::session::adapter_socket::server_link_state(&socket).await else {
65 return Ok(None);
66 };
67 // The registration is what a bind publishes and a run's end removes, so its
68 // mtime dates this client and not some earlier bind. A client that served
69 // a long time and republished late reads as young, which is the answer: the
70 // registration names the process, and a republished one is a restarted one.
71 let stamp = registration_path(layout.root());
72 let uptime = std::fs::metadata(&stamp)
73 .and_then(|meta| meta.modified())
74 .ok()
75 .and_then(|modified| std::time::SystemTime::now().duration_since(modified).ok())
76 .unwrap_or_default();
77 Ok(Some(StatusReport {
78 uptime,
79 faults: fault_count(&layout)?,
80 connected,
81 socket,
82 }))
83}
84
85/// Count the `session_fault` events recorded in the client database.
86pub fn fault_count(layout: &RoleWorkspace) -> Result<usize> {
87 let path = layout.client_db_path();
88 if !path.exists() {
89 return Ok(0);
90 }
91 let store = onlyne_store::ClientStore::open(&path)?;
92 let events = store.events_since(0, FAULT_SCAN_LIMIT)?;
93 Ok(events
94 .iter()
95 .filter(|event| event.kind == "session_fault")
96 .count())
97}