sd_switch/systemd/
systemctl.rs1use 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 pub fn new(system: bool) -> Result<SystemctlServiceManager, Error> {
160 Ok(SystemctlServiceManager { system })
161 }
162
163 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 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 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
239fn 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
277fn 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}