i3status_rs/blocks/
service_status.rs1use 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 .bytes()
140 .map(|b| {
141 if b.is_ascii_alphanumeric() {
142 char::from(b).to_string()
144 } else {
145 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}