Skip to main content

monitord/
varlink_system.rs

1//! # varlink_system module
2//!
3//! systemd version and overall system state via the `io.systemd.Manager`
4//! varlink API on PID 1's socket. Available from systemd v258+.
5//!
6//! `Describe` returns both values in a single call, so each collector asks for
7//! the one it needs rather than streaming the whole `io.systemd.Metrics` list
8//! (which also carries `Version`/`SystemState`, but costs ~40x the wall time
9//! because it enumerates every unit).
10//!
11//! PID 1 serves varlink requests one at a time, so a call issued while the
12//! units collector is streaming `io.systemd.Metrics.List` waits for that whole
13//! stream to finish. Both collectors therefore share one `Describe` per cycle
14//! (see [`shared_describe`]) instead of occupying PID 1 twice.
15
16use std::sync::Arc;
17
18use futures_util::future::{BoxFuture, FutureExt, Shared};
19use tokio::sync::RwLock;
20
21use crate::system::{parse_system_state, SystemdSystemState, SystemdVersion};
22use crate::varlink::manager::{Manager, ManagerRuntime};
23use crate::MachineStats;
24
25pub use crate::varlink::manager::MANAGER_SOCKET_PATH;
26
27/// One `Manager.Describe` call, awaited by every collector that needs it.
28///
29/// The first collector to poll it drives the call; the rest wait on that same
30/// result. `Shared` needs a cloneable output, hence the `Arc` around the error.
31pub type SharedDescribe = Shared<BoxFuture<'static, Result<ManagerRuntime, Arc<anyhow::Error>>>>;
32
33/// Call `io.systemd.Manager.Describe` and return its runtime section.
34///
35/// No `spawn_blocking` here, unlike the streaming `io.systemd.Metrics.List`
36/// call in `varlink_units`: a single-shot zlink call holds no `!Send` stream
37/// across an await, so it runs on the main runtime like any other future.
38async fn describe_runtime(
39    endpoint: &crate::varlink::endpoint::VarlinkEndpoint,
40) -> anyhow::Result<ManagerRuntime> {
41    let mut conn = endpoint.connect().await?;
42    match conn.describe().await? {
43        Ok(output) => output
44            .runtime
45            .ok_or_else(|| anyhow::anyhow!("io.systemd.Manager.Describe has no runtime")),
46        Err(e) => Err(anyhow::anyhow!("io.systemd.Manager.Describe error: {}", e)),
47    }
48}
49
50/// Prepare the shared `Describe` for one collection cycle.
51///
52/// Nothing is sent until a collector awaits it, so building this for a cycle
53/// that ends up needing neither value costs nothing.
54pub fn shared_describe(endpoint: crate::varlink::endpoint::VarlinkEndpoint) -> SharedDescribe {
55    async move { describe_runtime(&endpoint).await.map_err(Arc::new) }
56        .boxed()
57        .shared()
58}
59
60async fn describe(shared: SharedDescribe) -> anyhow::Result<ManagerRuntime> {
61    shared
62        .await
63        .map_err(|err| anyhow::anyhow!("{:#}", err))
64        .map_err(|err| err.context("io.systemd.Manager.Describe failed"))
65}
66
67pub async fn get_system_state(shared: SharedDescribe) -> anyhow::Result<SystemdSystemState> {
68    let runtime = describe(shared).await?;
69    let system_state = runtime
70        .system_state
71        .ok_or_else(|| anyhow::anyhow!("io.systemd.Manager.Describe has no SystemState"))?;
72    Ok(parse_system_state(&system_state))
73}
74
75pub async fn get_version(shared: SharedDescribe) -> anyhow::Result<SystemdVersion> {
76    let runtime = describe(shared).await?;
77    let version = runtime
78        .version
79        .ok_or_else(|| anyhow::anyhow!("io.systemd.Manager.Describe has no Version"))?;
80    Ok(version.try_into()?)
81}
82
83/// Async wrapper that can update the system state when passed a locked struct.
84///
85/// Like the D-Bus equivalents in `system.rs`, the varlink call completes before
86/// the write lock is taken so the shared `MachineStats` lock is never held
87/// across a round trip.
88pub async fn update_system_stats(
89    shared: SharedDescribe,
90    locked_machine_stats: Arc<RwLock<MachineStats>>,
91) -> anyhow::Result<()> {
92    let system_state = get_system_state(shared).await?;
93    let mut machine_stats = locked_machine_stats.write().await;
94    machine_stats.system_state = system_state;
95    Ok(())
96}
97
98/// Async wrapper that can update the systemd version when passed a locked struct.
99pub async fn update_version(
100    shared: SharedDescribe,
101    locked_machine_stats: Arc<RwLock<MachineStats>>,
102) -> anyhow::Result<()> {
103    let version = get_version(shared).await?;
104    let mut machine_stats = locked_machine_stats.write().await;
105    machine_stats.version = version;
106    Ok(())
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::varlink::manager::DescribeOutput;
113
114    #[test]
115    fn test_describe_output_ignores_unconsumed_fields() {
116        // Trimmed from a real systemd 262 reply: the full one is ~3KB of
117        // manager configuration we have no use for, and every field of it must
118        // deserialize away without error.
119        let reply = r#"{
120            "context": {"ShowStatus": true, "LogTarget": "journal"},
121            "runtime": {
122                "Version": "262~rc3-1.fc46",
123                "Architecture": "arm64",
124                "Virtualization": "docker",
125                "SystemState": "running",
126                "NNames": 295
127            }
128        }"#;
129        let output: DescribeOutput =
130            serde_json::from_str(reply).expect("Describe reply should deserialize");
131        let runtime = output.runtime.expect("reply should have a runtime section");
132        assert_eq!(runtime.version.as_deref(), Some("262~rc3-1.fc46"));
133        assert_eq!(runtime.system_state.as_deref(), Some("running"));
134    }
135
136    #[test]
137    fn test_version_string_parses_like_the_dbus_property() {
138        // Both APIs hand back the same string, so the D-Bus parser is reused.
139        let version: SystemdVersion = "262~rc3-1.fc46"
140            .to_string()
141            .try_into()
142            .expect("version should parse");
143        assert_eq!(
144            version,
145            SystemdVersion::new(262, "rc3-1".to_string(), None, "fc46".to_string())
146        );
147    }
148
149    #[tokio::test]
150    async fn test_shared_describe_resolves_for_every_awaiter() {
151        // Both collectors await one call, so a failure has to reach both of
152        // them — otherwise one would report success off a call never made.
153        let shared = shared_describe("/nonexistent/io.systemd.Manager".into());
154        let (version, system_state) =
155            tokio::join!(get_version(shared.clone()), get_system_state(shared));
156        assert!(version.is_err());
157        assert!(system_state.is_err());
158    }
159
160    #[test]
161    fn test_runtime_without_our_fields() {
162        // A systemd too old to report these leaves them absent rather than null.
163        let output: DescribeOutput =
164            serde_json::from_str(r#"{"runtime": {}}"#).expect("Describe reply should deserialize");
165        let runtime = output.runtime.expect("reply should have a runtime section");
166        assert!(runtime.version.is_none());
167        assert!(runtime.system_state.is_none());
168    }
169}