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    service_proxy: ServiceProxy<'static>,
112    active_state_changed: PropertyStream<'static, String>,
113}
114
115impl SystemdDriver {
116    async fn new(user: bool, service: String) -> Result<Self> {
117        let dbus_conn = if user {
118            new_dbus_connection().await?
119        } else {
120            new_system_dbus_connection().await?
121        };
122
123        if !service.is_ascii() {
124            return Err(Error::new(format!(
125                "service name \"{service}\" must only contain ASCII characters"
126            )));
127        }
128        let encoded_service = format!("{service}.service")
129            // For each byte...
130            .bytes()
131            .map(|b| {
132                if b.is_ascii_alphanumeric() {
133                    // Just use the character as a string
134                    char::from(b).to_string()
135                } else {
136                    // Otherwise use the hex representation of the byte preceded by an underscore
137                    format!("_{b:02x}")
138                }
139            })
140            .collect::<String>();
141
142        let path = format!("/org/freedesktop/systemd1/unit/{encoded_service}");
143
144        let proxy = UnitProxy::builder(&dbus_conn)
145            .path(path.clone())
146            .error("Could not set path")?
147            .build()
148            .await
149            .error("Failed to create UnitProxy")?;
150
151        let service_proxy = ServiceProxy::builder(&dbus_conn)
152            .path(path)
153            .error("Could not set path")?
154            .build()
155            .await
156            .error("Failed to create ServiceProxy")?;
157
158        Ok(Self {
159            active_state_changed: proxy.receive_active_state_changed().await,
160            proxy,
161            service_proxy,
162        })
163    }
164}
165
166#[async_trait]
167impl Driver for SystemdDriver {
168    async fn is_active(&self) -> Result<bool> {
169        let active_state = self
170            .proxy
171            .active_state()
172            .await
173            .error("Could not get active_state")?;
174
175        Ok(match &*active_state {
176            "active" => true,
177            "activating" => {
178                let service_type = self
179                    .service_proxy
180                    .type_()
181                    .await
182                    .error("Could not get service type")?;
183
184                service_type == "oneshot"
185            }
186            _ => false,
187        })
188    }
189
190    async fn wait_for_change(&mut self) -> Result<()> {
191        self.active_state_changed.next().await;
192        Ok(())
193    }
194}
195
196#[zbus::proxy(
197    interface = "org.freedesktop.systemd1.Unit",
198    default_service = "org.freedesktop.systemd1"
199)]
200trait Unit {
201    #[zbus(property)]
202    fn active_state(&self) -> zbus::Result<String>;
203}
204
205#[zbus::proxy(
206    interface = "org.freedesktop.systemd1.Service",
207    default_service = "org.freedesktop.systemd1"
208)]
209trait Service {
210    #[zbus(property, name = "Type")]
211    fn type_(&self) -> zbus::Result<String>;
212}