i3status_rs/blocks/
service_status.rs

1//! Display the status of a service
2//!
3//! Right now only `systemd` is supported.
4//!
5//! # Configuration
6//!
7//! Key | Values | Default
8//! ----|--------|--------
9//! `driver` | Which init system is running the service. Available drivers are: `"systemd"` | `"systemd"`
10//! `service` | The name of the service | **Required**
11//! `user` | If true, monitor the status of a user service instead of a system service. | `false`
12//! `active_format` | A string to customise the output of this block. See below for available placeholders. | `" $service active "`
13//! `inactive_format` | A string to customise the output of this block. See below for available placeholders. | `" $service inactive "`
14//! `active_state` | A valid [`State`] | [`State::Idle`]
15//! `inactive_state` | A valid [`State`]  | [`State::Critical`]
16//!
17//! Placeholder    | Value                     | Type   | Unit
18//! ---------------|---------------------------|--------|-----
19//! `service`      | The name of the service   | Text   | -
20//!
21//! # Example
22//!
23//! Example using an icon:
24//!
25//! ```toml
26//! [[block]]
27//! block = "service_status"
28//! service = "cups"
29//! active_format = " ^icon_tea "
30//! inactive_format = " no ^icon_tea "
31//! ```
32//!
33//! Example overriding the default `inactive_state`:
34//!
35//! ```toml
36//! [[block]]
37//! block = "service_status"
38//! service = "shadow"
39//! active_format = ""
40//! inactive_format = " Integrity of password and group files failed "
41//! inactive_state = "Warning"
42//! ```
43//!
44
45use super::prelude::*;
46use zbus::proxy::PropertyStream;
47
48#[derive(Deserialize, Debug, Default)]
49#[serde(deny_unknown_fields, default)]
50pub struct Config {
51    pub driver: DriverType,
52    pub service: String,
53    pub user: bool,
54    pub active_format: FormatConfig,
55    pub inactive_format: FormatConfig,
56    pub active_state: Option<State>,
57    pub inactive_state: Option<State>,
58}
59
60#[derive(Deserialize, Debug, SmartDefault)]
61#[serde(rename_all = "snake_case")]
62pub enum DriverType {
63    #[default]
64    Systemd,
65}
66
67pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
68    let active_format = config.active_format.with_default(" $service active ")?;
69    let inactive_format = config.inactive_format.with_default(" $service inactive ")?;
70
71    let active_state = config.active_state.unwrap_or(State::Idle);
72    let inactive_state = config.inactive_state.unwrap_or(State::Critical);
73
74    let mut driver: Box<dyn Driver> = match config.driver {
75        DriverType::Systemd => {
76            Box::new(SystemdDriver::new(config.user, config.service.clone()).await?)
77        }
78    };
79
80    loop {
81        let service_active_state = driver.is_active().await?;
82
83        let mut widget = Widget::new();
84
85        if service_active_state {
86            widget.state = active_state;
87            widget.set_format(active_format.clone());
88        } else {
89            widget.state = inactive_state;
90            widget.set_format(inactive_format.clone());
91        };
92
93        widget.set_values(map! {
94            "service" =>Value::text(config.service.clone()),
95        });
96
97        api.set_widget(widget)?;
98
99        driver.wait_for_change().await?;
100    }
101}
102
103#[async_trait]
104trait Driver {
105    async fn is_active(&self) -> Result<bool>;
106    async fn wait_for_change(&mut self) -> Result<()>;
107}
108
109struct SystemdDriver {
110    proxy: UnitProxy<'static>,
111    active_state_changed: PropertyStream<'static, String>,
112}
113
114impl SystemdDriver {
115    async fn new(user: bool, service: String) -> Result<Self> {
116        let dbus_conn = if user {
117            new_dbus_connection().await?
118        } else {
119            new_system_dbus_connection().await?
120        };
121
122        if !service.is_ascii() {
123            return Err(Error::new(format!(
124                "service name \"{service}\" must only contain ASCII characters"
125            )));
126        }
127        let encoded_service = format!("{service}.service")
128            // For each byte...
129            .bytes()
130            .map(|b| {
131                if b.is_ascii_alphanumeric() {
132                    // Just use the character as a string
133                    char::from(b).to_string()
134                } else {
135                    // Otherwise use the hex representation of the byte preceded by an underscore
136                    format!("_{b:02x}")
137                }
138            })
139            .collect::<String>();
140
141        let path = format!("/org/freedesktop/systemd1/unit/{encoded_service}");
142
143        let proxy = UnitProxy::builder(&dbus_conn)
144            .path(path)
145            .error("Could not set path")?
146            .build()
147            .await
148            .error("Failed to create UnitProxy")?;
149
150        Ok(Self {
151            active_state_changed: proxy.receive_active_state_changed().await,
152            proxy,
153        })
154    }
155}
156
157#[async_trait]
158impl Driver for SystemdDriver {
159    async fn is_active(&self) -> Result<bool> {
160        self.proxy
161            .active_state()
162            .await
163            .error("Could not get active_state")
164            .map(|state| state == "active")
165    }
166
167    async fn wait_for_change(&mut self) -> Result<()> {
168        self.active_state_changed.next().await;
169        Ok(())
170    }
171}
172
173#[zbus::proxy(
174    interface = "org.freedesktop.systemd1.Unit",
175    default_service = "org.freedesktop.systemd1"
176)]
177trait Unit {
178    #[zbus(property)]
179    fn active_state(&self) -> zbus::Result<String>;
180}