Skip to main content

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(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
68    BlockPlan::new(vec![
69        OutputPlan::new(
70            "active",
71            config.active_format.with_default(" $service active ")?,
72        ),
73        OutputPlan::new(
74            "inactive",
75            config.inactive_format.with_default(" $service inactive ")?,
76        ),
77    ])
78}
79
80pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
81    let output_active = plan.output("active")?;
82    let output_inactive = plan.output("inactive")?;
83
84    let active_state = config.active_state.unwrap_or(State::Idle);
85    let inactive_state = config.inactive_state.unwrap_or(State::Critical);
86
87    let mut driver: Box<dyn Driver> = match config.driver {
88        DriverType::Systemd => {
89            Box::new(SystemdDriver::new(config.user, config.service.clone()).await?)
90        }
91    };
92
93    loop {
94        let service_active_state = driver.is_active().await?;
95
96        let mut widget = if service_active_state {
97            output_active.new_widget().with_state(active_state)
98        } else {
99            output_inactive.new_widget().with_state(inactive_state)
100        };
101
102        widget.set_values(map! {
103            "service" =>Value::text(config.service.clone()),
104        });
105
106        api.set_widget(widget)?;
107
108        driver.wait_for_change().await?;
109    }
110}
111
112#[async_trait]
113trait Driver {
114    async fn is_active(&self) -> Result<bool>;
115    async fn wait_for_change(&mut self) -> Result<()>;
116}
117
118struct SystemdDriver {
119    proxy: UnitProxy<'static>,
120    service_proxy: ServiceProxy<'static>,
121    active_state_changed: PropertyStream<'static, String>,
122}
123
124impl SystemdDriver {
125    async fn new(user: bool, service: String) -> Result<Self> {
126        let dbus_conn = if user {
127            new_dbus_connection().await?
128        } else {
129            new_system_dbus_connection().await?
130        };
131
132        if !service.is_ascii() {
133            return Err(Error::new(format!(
134                "service name \"{service}\" must only contain ASCII characters"
135            )));
136        }
137        let encoded_service = format!("{service}.service")
138            // For each byte...
139            .bytes()
140            .map(|b| {
141                if b.is_ascii_alphanumeric() {
142                    // Just use the character as a string
143                    char::from(b).to_string()
144                } else {
145                    // Otherwise use the hex representation of the byte preceded by an underscore
146                    format!("_{b:02x}")
147                }
148            })
149            .collect::<String>();
150
151        let path = format!("/org/freedesktop/systemd1/unit/{encoded_service}");
152
153        let proxy = UnitProxy::builder(&dbus_conn)
154            .path(path.clone())
155            .error("Could not set path")?
156            .build()
157            .await
158            .error("Failed to create UnitProxy")?;
159
160        let service_proxy = ServiceProxy::builder(&dbus_conn)
161            .path(path)
162            .error("Could not set path")?
163            .build()
164            .await
165            .error("Failed to create ServiceProxy")?;
166
167        Ok(Self {
168            active_state_changed: proxy.receive_active_state_changed().await,
169            proxy,
170            service_proxy,
171        })
172    }
173}
174
175#[async_trait]
176impl Driver for SystemdDriver {
177    async fn is_active(&self) -> Result<bool> {
178        let active_state = self
179            .proxy
180            .active_state()
181            .await
182            .error("Could not get active_state")?;
183
184        Ok(match &*active_state {
185            "active" => true,
186            "activating" => {
187                let service_type = self
188                    .service_proxy
189                    .type_()
190                    .await
191                    .error("Could not get service type")?;
192
193                service_type == "oneshot"
194            }
195            _ => false,
196        })
197    }
198
199    async fn wait_for_change(&mut self) -> Result<()> {
200        self.active_state_changed.next().await;
201        Ok(())
202    }
203}
204
205#[zbus::proxy(
206    interface = "org.freedesktop.systemd1.Unit",
207    default_service = "org.freedesktop.systemd1"
208)]
209trait Unit {
210    #[zbus(property)]
211    fn active_state(&self) -> zbus::Result<String>;
212}
213
214#[zbus::proxy(
215    interface = "org.freedesktop.systemd1.Service",
216    default_service = "org.freedesktop.systemd1"
217)]
218trait Service {
219    #[zbus(property, name = "Type")]
220    fn type_(&self) -> zbus::Result<String>;
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn plan_declares_both_states_without_icons() {
229        let plan = prepare(&Config::default()).unwrap();
230        let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
231        assert_eq!(ids, ["active", "inactive"]);
232        for id in ["active", "inactive"] {
233            let output = plan.output(id).unwrap();
234            assert!(output.format().contains_key("service"));
235            assert_eq!(output.output().icon_placeholders().count(), 0);
236        }
237    }
238
239    #[test]
240    fn plan_uses_configured_formats() {
241        let config = Config {
242            active_format: " up ".parse().unwrap(),
243            inactive_format: " $service down ".parse().unwrap(),
244            ..Config::default()
245        };
246        let plan = prepare(&config).unwrap();
247        assert!(
248            !plan
249                .output("active")
250                .unwrap()
251                .format()
252                .contains_key("service")
253        );
254        assert!(
255            plan.output("inactive")
256                .unwrap()
257                .format()
258                .contains_key("service")
259        );
260    }
261}