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(socket_path: &str) -> anyhow::Result<ManagerRuntime> {
39    let mut conn = zlink::unix::connect(socket_path).await?;
40    match conn.describe().await? {
41        Ok(output) => output
42            .runtime
43            .ok_or_else(|| anyhow::anyhow!("io.systemd.Manager.Describe has no runtime")),
44        Err(e) => Err(anyhow::anyhow!("io.systemd.Manager.Describe error: {}", e)),
45    }
46}
47
48/// Prepare the shared `Describe` for one collection cycle.
49///
50/// Nothing is sent until a collector awaits it, so building this for a cycle
51/// that ends up needing neither value costs nothing.
52pub fn shared_describe(socket_path: String) -> SharedDescribe {
53    async move { describe_runtime(&socket_path).await.map_err(Arc::new) }
54        .boxed()
55        .shared()
56}
57
58async fn describe(shared: SharedDescribe) -> anyhow::Result<ManagerRuntime> {
59    shared
60        .await
61        .map_err(|err| anyhow::anyhow!("{:#}", err))
62        .map_err(|err| err.context("io.systemd.Manager.Describe failed"))
63}
64
65pub async fn get_system_state(shared: SharedDescribe) -> anyhow::Result<SystemdSystemState> {
66    let runtime = describe(shared).await?;
67    let system_state = runtime
68        .system_state
69        .ok_or_else(|| anyhow::anyhow!("io.systemd.Manager.Describe has no SystemState"))?;
70    Ok(parse_system_state(&system_state))
71}
72
73pub async fn get_version(shared: SharedDescribe) -> anyhow::Result<SystemdVersion> {
74    let runtime = describe(shared).await?;
75    let version = runtime
76        .version
77        .ok_or_else(|| anyhow::anyhow!("io.systemd.Manager.Describe has no Version"))?;
78    Ok(version.try_into()?)
79}
80
81/// Async wrapper that can update the system state when passed a locked struct.
82///
83/// Like the D-Bus equivalents in `system.rs`, the varlink call completes before
84/// the write lock is taken so the shared `MachineStats` lock is never held
85/// across a round trip.
86pub async fn update_system_stats(
87    shared: SharedDescribe,
88    locked_machine_stats: Arc<RwLock<MachineStats>>,
89) -> anyhow::Result<()> {
90    let system_state = get_system_state(shared).await?;
91    let mut machine_stats = locked_machine_stats.write().await;
92    machine_stats.system_state = system_state;
93    Ok(())
94}
95
96/// Async wrapper that can update the systemd version when passed a locked struct.
97pub async fn update_version(
98    shared: SharedDescribe,
99    locked_machine_stats: Arc<RwLock<MachineStats>>,
100) -> anyhow::Result<()> {
101    let version = get_version(shared).await?;
102    let mut machine_stats = locked_machine_stats.write().await;
103    machine_stats.version = version;
104    Ok(())
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::varlink::manager::DescribeOutput;
111
112    #[test]
113    fn test_describe_output_ignores_unconsumed_fields() {
114        // Trimmed from a real systemd 262 reply: the full one is ~3KB of
115        // manager configuration we have no use for, and every field of it must
116        // deserialize away without error.
117        let reply = r#"{
118            "context": {"ShowStatus": true, "LogTarget": "journal"},
119            "runtime": {
120                "Version": "262~rc3-1.fc46",
121                "Architecture": "arm64",
122                "Virtualization": "docker",
123                "SystemState": "running",
124                "NNames": 295
125            }
126        }"#;
127        let output: DescribeOutput =
128            serde_json::from_str(reply).expect("Describe reply should deserialize");
129        let runtime = output.runtime.expect("reply should have a runtime section");
130        assert_eq!(runtime.version.as_deref(), Some("262~rc3-1.fc46"));
131        assert_eq!(runtime.system_state.as_deref(), Some("running"));
132    }
133
134    #[test]
135    fn test_version_string_parses_like_the_dbus_property() {
136        // Both APIs hand back the same string, so the D-Bus parser is reused.
137        let version: SystemdVersion = "262~rc3-1.fc46"
138            .to_string()
139            .try_into()
140            .expect("version should parse");
141        assert_eq!(
142            version,
143            SystemdVersion::new(262, "rc3-1".to_string(), None, "fc46".to_string())
144        );
145    }
146
147    #[tokio::test]
148    async fn test_shared_describe_resolves_for_every_awaiter() {
149        // Both collectors await one call, so a failure has to reach both of
150        // them — otherwise one would report success off a call never made.
151        let shared = shared_describe("/nonexistent/io.systemd.Manager".to_string());
152        let (version, system_state) =
153            tokio::join!(get_version(shared.clone()), get_system_state(shared));
154        assert!(version.is_err());
155        assert!(system_state.is_err());
156    }
157
158    #[test]
159    fn test_runtime_without_our_fields() {
160        // A systemd too old to report these leaves them absent rather than null.
161        let output: DescribeOutput =
162            serde_json::from_str(r#"{"runtime": {}}"#).expect("Describe reply should deserialize");
163        let runtime = output.runtime.expect("reply should have a runtime section");
164        assert!(runtime.version.is_none());
165        assert!(runtime.system_state.is_none());
166    }
167}