sd_switch/systemd/
systemctl.rs

1use crate::systemd::UnitManager;
2use crate::systemd::UnitStatus;
3
4use crate::error::Error;
5use std::collections::HashMap;
6use std::process;
7use std::str::FromStr;
8use std::sync::mpsc;
9use std::thread;
10use std::{result::Result, time::Duration};
11
12use super::SystemStatus;
13
14pub struct SystemctlServiceManager {
15    system: bool,
16}
17
18pub struct SystemctlUnitManager {
19    status: SystemctlUnitStatus,
20}
21
22#[derive(Clone, Debug, PartialEq)]
23pub struct SystemctlUnitStatus {
24    name: String,
25    description: String,
26    active_state: String,
27    address: String,
28    refuse_manual_start: bool,
29    refuse_manual_stop: bool,
30}
31
32impl UnitStatus for SystemctlUnitStatus {
33    fn name(&self) -> &str {
34        &self.name
35    }
36
37    fn description(&self) -> &str {
38        &self.description
39    }
40
41    fn active_state(&self) -> &str {
42        &self.active_state
43    }
44}
45
46struct Job {
47    unit_name: String,
48    child: process::Child,
49}
50
51pub struct SystemctlJobSet<'a> {
52    manager: &'a SystemctlServiceManager,
53    jobs: Vec<Job>,
54}
55
56impl SystemctlJobSet<'_> {
57    fn new(manager: &SystemctlServiceManager) -> SystemctlJobSet {
58        SystemctlJobSet {
59            manager,
60            jobs: Vec::new(),
61        }
62    }
63}
64
65impl super::JobSet for SystemctlJobSet<'_> {
66    fn reload_unit(&mut self, unit_name: &str) -> Result<(), Error> {
67        let child = self
68            .manager
69            .command()
70            .arg("--job-mode=replace")
71            .arg("reload")
72            .arg(unit_name)
73            .spawn()?;
74        self.jobs.push(Job {
75            unit_name: unit_name.to_string(),
76            child,
77        });
78        Ok(())
79    }
80
81    fn restart_unit(&mut self, unit_name: &str) -> Result<(), Error> {
82        let child = self
83            .manager
84            .command()
85            .arg("--job-mode=replace")
86            .arg("restart")
87            .arg(unit_name)
88            .spawn()?;
89        self.jobs.push(Job {
90            unit_name: unit_name.to_string(),
91            child,
92        });
93        Ok(())
94    }
95
96    fn start_unit(&mut self, unit_name: &str) -> Result<(), Error> {
97        let child = self
98            .manager
99            .command()
100            .arg("--job-mode=replace")
101            .arg("start")
102            .arg(unit_name)
103            .spawn()?;
104        self.jobs.push(Job {
105            unit_name: unit_name.to_string(),
106            child,
107        });
108        Ok(())
109    }
110
111    fn stop_unit(&mut self, unit_name: &str) -> Result<(), Error> {
112        let child = self
113            .manager
114            .command()
115            .arg("--job-mode=replace")
116            .arg("stop")
117            .arg(unit_name)
118            .spawn()?;
119        self.jobs.push(Job {
120            unit_name: unit_name.to_string(),
121            child,
122        });
123        Ok(())
124    }
125
126    fn wait_for_all<F>(&mut self, job_handler: F, timeout: Duration) -> Result<(), Error>
127    where
128        F: Fn(&str, &str) + Send + 'static,
129    {
130        if self.jobs.is_empty() {
131            return Ok(());
132        }
133
134        let (tx, rx) = mpsc::channel();
135
136        let jobs: Vec<Job> = self.jobs.drain(..).collect();
137        let _ = thread::spawn(move || {
138            for mut job in jobs {
139                let result = if job.child.wait().is_ok_and(|r| r.success()) {
140                    "done"
141                } else {
142                    "failed"
143                };
144                job_handler(&job.unit_name, result);
145            }
146
147            let _ = tx.send(());
148        });
149
150        rx.recv_timeout(timeout)
151            .map_err(|e| Error::SdSwitch(e.to_string()))
152    }
153}
154
155impl SystemctlServiceManager {
156    /// Creates a new systemctl service manager instance. The given Boolean
157    /// indicates whether we should connect to the system or user service
158    /// manager.
159    pub fn new(system: bool) -> Result<SystemctlServiceManager, Error> {
160        Ok(SystemctlServiceManager { system })
161    }
162
163    /// Start a systemctl command call, connecting to the system or user
164    /// manager.
165    fn command(&self) -> process::Command {
166        let mut cmd = process::Command::new("systemctl");
167
168        cmd.arg(if self.system { "--system" } else { "--user" });
169
170        cmd
171    }
172}
173
174impl<'a> super::ServiceManager for &'a SystemctlServiceManager {
175    type UnitManager = SystemctlUnitManager;
176    type UnitStatus = SystemctlUnitStatus;
177    type JobSet = SystemctlJobSet<'a>;
178
179    fn system_status(&self) -> Result<SystemStatus, Error> {
180        let result = self.command().arg("is-system-running").output()?;
181
182        let output = String::from_utf8_lossy(&result.stdout);
183
184        SystemStatus::from_str(output.trim_end())
185    }
186
187    /// Performs a systemd daemon reload, blocking until complete.
188    fn daemon_reload(&self) -> Result<(), Error> {
189        let result = self.command().arg("daemon-reload").status()?;
190
191        if result.success() {
192            Ok(())
193        } else {
194            Err(Error::SdSwitch(String::from(
195                "Error performing daemon reload",
196            )))
197        }
198    }
199
200    fn reset_failed(&self) -> Result<(), Error> {
201        let result = self.command().arg("reset-failed").status()?;
202
203        if result.success() {
204            Ok(())
205        } else {
206            Err(Error::SdSwitch(String::from(
207                "Error resetting failed units",
208            )))
209        }
210    }
211
212    /// Builds a unit manager for the unit with the given status.
213    fn unit_manager(&self, status: &SystemctlUnitStatus) -> Result<SystemctlUnitManager, Error> {
214        Ok(SystemctlUnitManager {
215            status: status.clone(),
216        })
217    }
218
219    fn new_job_set(&self) -> Result<SystemctlJobSet<'a>, Error> {
220        Ok(SystemctlJobSet::new(self))
221    }
222
223    fn list_units_by_states(&self, states: &[&str]) -> Result<Vec<SystemctlUnitStatus>, Error> {
224        let mut command = self.command();
225
226        command.arg("show").arg("*");
227
228        if !states.is_empty() {
229            command.arg("--state").arg(states.join(","));
230        }
231
232        let output = command.output()?;
233        let output = String::from_utf8_lossy(&output.stdout);
234
235        read_show_units(&output)
236    }
237}
238
239/// Reads the output of `systemctl show` for one unit. Stops at empty line of EOF.
240fn read_show_unit(kvs: &str) -> Result<SystemctlUnitStatus, Error> {
241    let new_error =
242        |msg| move || Error::SdSwitch(format!("Unexpected output from systemctl show: {msg}"));
243    let missing_field = |field| move || new_error(format!("Missing '{field}' field"))();
244    let unit = kvs
245        .lines()
246        .map(|line| {
247            line.split_once('=')
248                .ok_or_else(new_error(format!("Invalid unit line: {line}")))
249        })
250        .collect::<Result<HashMap<&str, &str>, Error>>()?;
251
252    let name = unit.get("Id").ok_or_else(missing_field("Id"))?;
253    let description = unit
254        .get("Description")
255        .ok_or_else(missing_field("Description"))?;
256    let active_state = unit
257        .get("ActiveState")
258        .ok_or_else(missing_field("ActiveState"))?;
259    let address = unit.get("Id").ok_or_else(missing_field("Id"))?;
260    let refuse_manual_start = unit
261        .get("RefuseManualStart")
262        .ok_or_else(missing_field("RefuseManualStart"))?;
263    let refuse_manual_stop = unit
264        .get("RefuseManualStop")
265        .ok_or_else(missing_field("RefuseManualStop"))?;
266
267    Ok(SystemctlUnitStatus {
268        name: (*name).to_string(),
269        description: (*description).to_string(),
270        active_state: (*active_state).to_string(),
271        address: (*address).to_string(),
272        refuse_manual_start: *refuse_manual_start == "yes",
273        refuse_manual_stop: *refuse_manual_stop == "yes",
274    })
275}
276
277/// Reads the complete output of `systemctl show`, keeping only the ones with the given states.
278fn read_show_units(handle: &str) -> Result<Vec<SystemctlUnitStatus>, Error> {
279    handle.split("\n\n").map(read_show_unit).collect()
280}
281
282impl UnitManager for SystemctlUnitManager {
283    fn refuse_manual_start(&self) -> Result<bool, Error> {
284        Ok(self.status.refuse_manual_start)
285    }
286
287    fn refuse_manual_stop(&self) -> Result<bool, Error> {
288        Ok(self.status.refuse_manual_stop)
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn can_read_show_units() -> Result<(), Error> {
298        let raw = r"Id=service1.service
299Description=Service 1
300ActiveState=active
301Type=simple
302RefuseManualStart=yes
303RefuseManualStop=no
304
305Id=service2.service
306Description=Service 2
307ActiveState=active
308Type=simple
309RefuseManualStart=no
310RefuseManualStop=yes
311";
312
313        let statuses = read_show_units(raw)?;
314
315        assert_eq!(
316            statuses,
317            vec![
318                SystemctlUnitStatus {
319                    name: "service1.service".to_string(),
320                    description: "Service 1".to_string(),
321                    active_state: "active".to_string(),
322                    address: "service1.service".to_string(),
323                    refuse_manual_start: true,
324                    refuse_manual_stop: false
325                },
326                SystemctlUnitStatus {
327                    name: "service2.service".to_string(),
328                    description: "Service 2".to_string(),
329                    active_state: "active".to_string(),
330                    address: "service2.service".to_string(),
331                    refuse_manual_start: false,
332                    refuse_manual_stop: true
333                }
334            ]
335        );
336
337        Ok(())
338    }
339}