Skip to main content

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}