systemctl/
lib.rs

1//! Crate to manage and monitor services through `systemctl`
2//! Homepage: <https://github.com/gwbres/systemctl>
3#![doc=include_str!("../README.md")]
4use std::io::{Error, ErrorKind};
5use std::process::{Child, ExitStatus};
6use std::str::FromStr;
7use strum_macros::EnumString;
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl";
13
14use bon::Builder;
15
16mod service_property;
17pub use service_property::ServiceProperty;
18
19/// Struct with API calls to systemctl.
20///
21/// Use the `::default()` impl if you don't need special arguments.
22///
23/// Use the builder API when you want to specify a custom path to systemctl binary or extra args.
24#[derive(Builder, Default, Clone, Debug)]
25pub struct SystemCtl {
26    /// Allows passing global arguments to systemctl like `--user`.
27    additional_args: Vec<String>,
28    /// The path to the systemctl binary, by default it's [SYSTEMCTL_PATH]
29    path: Option<String>,
30}
31
32impl SystemCtl {
33    /// Invokes `systemctl $args`
34    fn spawn_child<'a, 's: 'a, S: IntoIterator<Item = &'a str>>(
35        &'s self,
36        args: S,
37    ) -> std::io::Result<Child> {
38        std::process::Command::new(self.get_path())
39            .args(self.additional_args.iter().map(String::as_str).chain(args))
40            .stdout(std::process::Stdio::piped())
41            .stderr(std::process::Stdio::null())
42            .spawn()
43    }
44
45    fn get_path(&self) -> &str {
46        self.path.as_deref().unwrap_or(SYSTEMCTL_PATH)
47    }
48
49    /// Invokes `systemctl $args` silently
50    fn systemctl<'a, 's: 'a, S: IntoIterator<Item = &'a str>>(
51        &'s self,
52        args: S,
53    ) -> std::io::Result<ExitStatus> {
54        self.spawn_child(args)?.wait()
55    }
56
57    /// Invokes `systemctl $args` and captures stdout stream
58    fn systemctl_capture<'a, 's: 'a, S: IntoIterator<Item = &'a str>>(
59        &'s self,
60        args: S,
61    ) -> std::io::Result<String> {
62        let child = self.spawn_child(args)?;
63        let output = child.wait_with_output()?;
64        match output.status.code() {
65            Some(0) => {}, // success
66            Some(1) => {}, // success -> Ok(Unit not found)
67            Some(3) => {}, // success -> Ok(unit is inactive and/or dead)
68            Some(4) => {
69                return Err(Error::new(
70                    ErrorKind::PermissionDenied,
71                    "Missing privileges or unit not found",
72                ))
73            },
74            // unknown errorcodes
75            Some(code) => {
76                // TODO: Maybe a better ErrorKind, none really seem to fit
77                return Err(Error::other(format!("Process exited with code: {code}")));
78            },
79            None => {
80                return Err(Error::new(
81                    ErrorKind::Interrupted,
82                    "Process terminated by signal",
83                ))
84            },
85        }
86
87        let stdout: Vec<u8> = output.stdout;
88        let size = stdout.len();
89
90        if size > 0 {
91            return if let Ok(s) = String::from_utf8(stdout) {
92                Ok(s)
93            } else {
94                Err(Error::new(
95                    ErrorKind::InvalidData,
96                    "Invalid utf8 data in stdout",
97                ))
98            };
99        }
100
101        // if this is reached all if's above did not work
102        Err(Error::new(
103            ErrorKind::UnexpectedEof,
104            "systemctl stdout empty",
105        ))
106    }
107
108    /// Reloads all unit files
109    pub fn daemon_reload(&self) -> std::io::Result<ExitStatus> {
110        self.systemctl(["daemon-reload"])
111    }
112
113    /// Forces given `unit` to (re)start
114    pub fn restart(&self, unit: &str) -> std::io::Result<ExitStatus> {
115        self.systemctl(["restart", unit])
116    }
117
118    /// Forces given `unit` to start
119    pub fn start(&self, unit: &str) -> std::io::Result<ExitStatus> {
120        self.systemctl(["start", unit])
121    }
122
123    /// Forces given `unit` to stop
124    pub fn stop(&self, unit: &str) -> std::io::Result<ExitStatus> {
125        self.systemctl(["stop", unit])
126    }
127
128    /// Triggers reload for given `unit`
129    pub fn reload(&self, unit: &str) -> std::io::Result<ExitStatus> {
130        self.systemctl(["reload", unit])
131    }
132
133    /// Triggers reload or restarts given `unit`
134    pub fn reload_or_restart(&self, unit: &str) -> std::io::Result<ExitStatus> {
135        self.systemctl(["reload-or-restart", unit])
136    }
137
138    /// Enable given `unit` to start at boot
139    pub fn enable(&self, unit: &str) -> std::io::Result<ExitStatus> {
140        self.systemctl(["enable", unit])
141    }
142
143    /// Disable given `unit` to start at boot
144    pub fn disable(&self, unit: &str) -> std::io::Result<ExitStatus> {
145        self.systemctl(["disable", unit])
146    }
147
148    /// Returns raw status from `systemctl status $unit` call
149    pub fn status(&self, unit: &str) -> std::io::Result<String> {
150        self.systemctl_capture(["status", unit])
151    }
152
153    /// Invokes systemctl `cat` on given `unit`
154    pub fn cat(&self, unit: &str) -> std::io::Result<String> {
155        self.systemctl_capture(["cat", unit])
156    }
157
158    /// Returns `true` if given `unit` is actively running
159    pub fn is_active(&self, unit: &str) -> std::io::Result<bool> {
160        let status = self.systemctl_capture(["is-active", unit])?;
161        Ok(status.trim_end().eq("active"))
162    }
163
164    /// Returns active state of the given `unit`
165    pub fn get_active_state(&self, unit: &str) -> std::io::Result<ActiveState> {
166        let status = self.systemctl_capture(vec!["is-active", unit])?;
167        ActiveState::from_str(status.trim_end()).map_or(
168            Err(Error::new(
169                ErrorKind::InvalidData,
170                format!("Invalid status {}", status),
171            )),
172            Ok,
173        )
174    }
175
176    /// Returns a list of services that are dependencies of the given unit
177    pub fn list_dependencies(&self, unit: &str) -> std::io::Result<Vec<String>> {
178        let output = self.systemctl_capture(vec!["list-dependencies", unit])?;
179        Self::list_dependencies_from_raw(output)
180    }
181
182    pub fn list_dependencies_from_raw(raw: String) -> std::io::Result<Vec<String>> {
183        let mut dependencies = Vec::<String>::new();
184        for line in raw.lines().skip(1) {
185            dependencies.push(String::from(
186                line.replace(|c: char| !c.is_ascii(), "").trim(),
187            ));
188        }
189        Ok(dependencies)
190    }
191
192    /// Isolates given unit, only self and its dependencies are
193    /// now actively running
194    pub fn isolate(&self, unit: &str) -> std::io::Result<ExitStatus> {
195        self.systemctl(["isolate", unit])
196    }
197
198    /// Freezes (halts) given unit.
199    /// This operation might not be feasible.
200    pub fn freeze(&self, unit: &str) -> std::io::Result<ExitStatus> {
201        self.systemctl(["freeze", unit])
202    }
203
204    /// Unfreezes given unit (recover from halted state).
205    /// This operation might not be feasible.
206    pub fn unfreeze(&self, unit: &str) -> std::io::Result<ExitStatus> {
207        self.systemctl(["thaw", unit])
208    }
209
210    /// Returns `true` if given `unit` exists,
211    /// ie., service could be or is actively deployed
212    /// and manageable by systemd
213    pub fn exists(&self, unit: &str) -> std::io::Result<bool> {
214        let unit_list = self.list_unit_files(None, None, Some(unit))?;
215        Ok(!unit_list.is_empty())
216    }
217
218    /// Returns a `Vector` of `UnitList` structs extracted from systemctl listing.
219    ///  + type filter: optional `--type` filter
220    ///  + state filter: optional `--state` filter
221    ///  + glob filter: optional unit name filter
222    pub fn list_unit_files_full(
223        &self,
224        type_filter: Option<&str>,
225        state_filter: Option<&str>,
226        glob: Option<&str>,
227    ) -> std::io::Result<Vec<UnitList>> {
228        let mut args = vec!["list-unit-files"];
229        if let Some(filter) = type_filter {
230            args.push("--type");
231            args.push(filter)
232        }
233        if let Some(filter) = state_filter {
234            args.push("--state");
235            args.push(filter)
236        }
237        if let Some(glob) = glob {
238            args.push(glob)
239        }
240        let mut result: Vec<UnitList> = Vec::new();
241        let content = self.systemctl_capture(args)?;
242        let lines = content
243            .lines()
244            .filter(|line| line.contains('.') && !line.ends_with('.'));
245
246        for l in lines {
247            let parsed: Vec<&str> = l.split_ascii_whitespace().collect();
248            let vendor_preset = match parsed[2] {
249                "-" => None,
250                "enabled" => Some(true),
251                "disabled" => Some(false),
252                _ => None,
253            };
254            result.push(UnitList {
255                unit_file: parsed[0].to_string(),
256                state: parsed[1].to_string(),
257                vendor_preset,
258            })
259        }
260        Ok(result)
261    }
262
263    /// Returns a `Vector` of `UnitService` structs extracted from systemctl listing.
264    ///  + type filter: optional `--type` filter
265    ///  + state filter: optional `--state` filter
266    ///  + glob filter: optional unit name filter
267    pub fn list_units_full(
268        &self,
269        type_filter: Option<&str>,
270        state_filter: Option<&str>,
271        glob: Option<&str>,
272    ) -> std::io::Result<Vec<UnitService>> {
273        let mut args = vec!["list-units"];
274        if let Some(filter) = type_filter {
275            args.push("--type");
276            args.push(filter)
277        }
278        if let Some(filter) = state_filter {
279            args.push("--state");
280            args.push(filter)
281        }
282        if let Some(glob) = glob {
283            args.push(glob)
284        }
285        let content = self.systemctl_capture(args)?;
286        Self::list_units_full_from_raw(content)
287    }
288
289    pub fn list_units_full_from_raw(raw: String) -> std::io::Result<Vec<UnitService>> {
290        let mut result: Vec<UnitService> = Vec::new();
291
292        let lines = raw
293            .lines()
294            .filter(|line| line.contains('.') && !line.ends_with('.'));
295
296        for l in lines {
297            // fixes format for not found units
298            let slice = if l.starts_with("● ") { &l[3..] } else { l };
299            let parsed: Vec<&str> = slice.split_ascii_whitespace().collect();
300
301            result.push(UnitService {
302                unit_name: parsed[0].to_string(),
303                loaded: LoadedState::from_str(parsed[1]).unwrap_or(LoadedState::Unknown),
304                active: ActiveState::from_str(parsed[2]).unwrap_or(ActiveState::Unknown),
305                sub_state: parsed[3].to_string(),
306                description: parsed[4..].join(" "),
307            })
308        }
309        Ok(result)
310    }
311
312    /// Returns a `Vector` of unit names extracted from systemctl listing.
313    ///  + type filter: optional `--type` filter
314    ///  + state filter: optional `--state` filter
315    ///  + glob filter: optional unit name filter
316    pub fn list_unit_files(
317        &self,
318        type_filter: Option<&str>,
319        state_filter: Option<&str>,
320        glob: Option<&str>,
321    ) -> std::io::Result<Vec<String>> {
322        let list = self.list_unit_files_full(type_filter, state_filter, glob);
323        Ok(list?.iter().map(|n| n.unit_file.clone()).collect())
324    }
325
326    /// Returns a `Vector` of unit names extracted from systemctl listing.
327    ///  + type filter: optional `--type` filter
328    ///  + state filter: optional `--state` filter
329    ///  + glob filter: optional unit name filter
330    pub fn list_units(
331        &self,
332        type_filter: Option<&str>,
333        state_filter: Option<&str>,
334        glob: Option<&str>,
335    ) -> std::io::Result<Vec<String>> {
336        let list = self.list_units_full(type_filter, state_filter, glob);
337        Ok(list?.iter().map(|n| n.unit_name.clone()).collect())
338    }
339
340    /// Returns list of services that are currently declared as running
341    pub fn list_running_services(&self) -> std::io::Result<Vec<String>> {
342        self.list_units(Some("service"), Some("running"), None)
343    }
344
345    /// Returns list of services that are currently declared as failed
346    pub fn list_failed_services(&self) -> std::io::Result<Vec<String>> {
347        self.list_units(Some("service"), Some("failed"), None)
348    }
349
350    /// Returns list of services that are currently declared as disabled
351    pub fn list_disabled_services(&self) -> std::io::Result<Vec<String>> {
352        self.list_unit_files(Some("service"), Some("disabled"), None)
353    }
354
355    /// Returns list of services that are currently declared as enabled
356    pub fn list_enabled_services(&self) -> std::io::Result<Vec<String>> {
357        self.list_unit_files(Some("service"), Some("enabled"), None)
358    }
359
360    /// Builds a new `Unit` structure by retrieving
361    /// structure attributes with a `systemctl status $unit` call
362    pub fn create_unit(&self, name: &str) -> std::io::Result<Unit> {
363        if let Ok(false) = self.exists(name) {
364            return Err(Error::new(
365                ErrorKind::NotFound,
366                format!("Unit or service \"{}\" does not exist", name),
367            ));
368        }
369        let mut u = Unit::default();
370        let status = self.status(name)?;
371        let mut lines = status.lines();
372        let next = lines.next().unwrap();
373        let (_, rem) = next.split_at(3);
374        let mut items = rem.split_ascii_whitespace();
375        let name_raw = items.next().unwrap().trim();
376        if let Some(delim) = items.next() {
377            if delim.trim().eq("-") {
378                // --> description string is provided
379                let items: Vec<_> = items.collect();
380                u.description = Some(itertools::join(&items, " "));
381            }
382        }
383        let (name, utype_raw) = name_raw
384            .rsplit_once('.')
385            .expect("Unit is missing a Type, this should not happen!");
386        // `type` is deduced from .extension
387        u.utype = match Type::from_str(utype_raw) {
388            Ok(t) => t,
389            Err(e) => panic!("For {:?} -> {e}", name_raw),
390        };
391        let mut is_doc = false;
392        for line in lines {
393            let line = line.trim_start();
394            if let Some(line) = line.strip_prefix("Loaded: ") {
395                // Match and get rid of "Loaded: "
396                if let Some(line) = line.strip_prefix("loaded ") {
397                    u.loaded_state = LoadedState::Loaded;
398                    let line = line.strip_prefix('(').unwrap();
399                    let line = line.strip_suffix(')').unwrap();
400                    let items: Vec<&str> = line.split(';').collect();
401                    u.script = items[0].trim().to_string();
402                    u.auto_start = AutoStartStatus::from_str(items[1].trim())
403                        .unwrap_or(AutoStartStatus::Disabled);
404                    if items.len() > 2 {
405                        // preset is optionnal ?
406                        u.preset = items[2].trim().ends_with("enabled");
407                    }
408                } else if line.starts_with("masked") {
409                    u.loaded_state = LoadedState::Masked;
410                }
411            } else if let Some(line) = line.strip_prefix("Transient: ") {
412                if line == "yes" {
413                    u.transient = true
414                }
415            } else if line.starts_with("Active: ") {
416                // skip that one
417                // we already have .active() .inative() methods
418                // to access this information
419            } else if let Some(line) = line.strip_prefix("Docs: ") {
420                is_doc = true;
421                if let Ok(doc) = Doc::from_str(line) {
422                    u.docs.get_or_insert_with(Vec::new).push(doc);
423                }
424            } else if let Some(line) = line.strip_prefix("What: ") {
425                // mountpoint infos
426                u.mounted = Some(line.to_string())
427            } else if let Some(line) = line.strip_prefix("Where: ") {
428                // mountpoint infos
429                u.mountpoint = Some(line.to_string());
430            } else if let Some(line) = line.strip_prefix("Main PID: ") {
431                // example -> Main PID: 787 (gpm)
432                if let Some((pid, proc)) = line.split_once(' ') {
433                    u.pid = Some(pid.parse::<u64>().unwrap_or(0));
434                    u.process = Some(proc.replace(&['(', ')'][..], ""));
435                };
436            } else if let Some(line) = line.strip_prefix("Cntrl PID: ") {
437                // example -> Main PID: 787 (gpm)
438                if let Some((pid, proc)) = line.split_once(' ') {
439                    u.pid = Some(pid.parse::<u64>().unwrap_or(0));
440                    u.process = Some(proc.replace(&['(', ')'][..], ""));
441                };
442            } else if line.starts_with("Process: ") {
443                //TODO: implement
444                //TODO: parse as a Process item
445                //let items : Vec<_> = line.split_ascii_whitespace().collect();
446                //let proc_pid = u64::from_str_radix(items[1].trim(), 10).unwrap();
447                //let cli;
448                //Process: 640 ExecStartPre=/usr/sbin/sshd -t (code=exited, status=0/SUCCESS)
449            } else if line.starts_with("CGroup: ") {
450                //TODO: implement
451                //LINE: "CGroup: /system.slice/sshd.service"
452                //LINE: "└─1050 /usr/sbin/sshd -D"
453            } else if line.starts_with("Tasks: ") {
454                //TODO: implement
455            } else if let Some(line) = line.strip_prefix("Memory: ") {
456                u.memory = Some(line.trim().to_string());
457            } else if let Some(line) = line.strip_prefix("CPU: ") {
458                u.cpu = Some(line.trim().to_string())
459            } else {
460                // handling multi line cases
461                if is_doc {
462                    let line = line.trim_start();
463                    if let Ok(doc) = Doc::from_str(line) {
464                        u.docs.get_or_insert_with(Vec::new).push(doc);
465                    }
466                }
467            }
468        }
469
470        if let Ok(content) = self.cat(name_raw) {
471            let line_tuple = content
472                .lines()
473                .filter_map(|line| line.split_once('=').to_owned());
474            for (k, v) in line_tuple {
475                let val = v.to_string();
476                match k {
477                    "Wants" => u.wants.get_or_insert_with(Vec::new).push(val),
478                    "WantedBy" => u.wanted_by.get_or_insert_with(Vec::new).push(val),
479                    "Also" => u.also.get_or_insert_with(Vec::new).push(val),
480                    "Before" => u.before.get_or_insert_with(Vec::new).push(val),
481                    "After" => u.after.get_or_insert_with(Vec::new).push(val),
482                    "ExecStart" => u.exec_start = Some(val),
483                    "ExecReload" => u.exec_reload = Some(val),
484                    "Restart" => u.restart_policy = Some(val),
485                    "KillMode" => u.kill_mode = Some(val),
486                    _ => {},
487                }
488            }
489        }
490
491        u.active = self.is_active(name_raw)?;
492        u.name = name.to_string();
493        Ok(u)
494    }
495
496    /// Show service property using systemctl show --property
497    pub fn show(&self, property: ServiceProperty, unit: &str) -> std::io::Result<Option<String>> {
498        let mut content =
499            self.systemctl_capture(["show", "--property", property.into(), "--value", unit])?;
500        if content.ends_with('\n') {
501            // remove line break at the end of the line, but keep other whitespaces
502            content.pop();
503        }
504        Ok(if content.as_str() != "[not set]" {
505            Some(content)
506        } else {
507            None
508        })
509    }
510}
511
512#[derive(Clone, Debug, Default, PartialEq)]
513#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
514/// Implementation of list generated with
515/// `systemctl list-unit-files`
516pub struct UnitList {
517    /// Unit name: `name.type`
518    pub unit_file: String,
519    /// Unit state
520    pub state: String,
521    /// Unit vendor preset
522    pub vendor_preset: Option<bool>,
523}
524
525#[derive(Clone, Debug, Default, PartialEq)]
526#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
527/// Implementation of list generated with
528/// `systemctl list-units`
529pub struct UnitService {
530    /// Unit name: `name.type`
531    pub unit_name: String,
532    /// Loaded state
533    pub loaded: LoadedState,
534    /// Unit state
535    pub active: ActiveState,
536    /// Unit substate
537    pub sub_state: String,
538    /// Unit description
539    pub description: String,
540}
541
542/// `AutoStartStatus` describes the Unit current state
543#[derive(Copy, Clone, PartialEq, Eq, EnumString, Debug, Default)]
544#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
545pub enum AutoStartStatus {
546    #[strum(serialize = "static")]
547    Static,
548    #[strum(serialize = "enabled")]
549    Enabled,
550    #[strum(serialize = "enabled-runtime")]
551    EnabledRuntime,
552    #[strum(serialize = "disabled")]
553    #[default]
554    Disabled,
555    #[strum(serialize = "generated")]
556    Generated,
557    #[strum(serialize = "indirect")]
558    Indirect,
559    #[strum(serialize = "transient")]
560    Transient,
561}
562
563/// `Type` describes a Unit declaration Type in systemd
564#[derive(Copy, Clone, PartialEq, Eq, EnumString, Debug, Default)]
565#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
566pub enum Type {
567    #[strum(serialize = "automount")]
568    AutoMount,
569    #[strum(serialize = "mount")]
570    Mount,
571    #[strum(serialize = "service")]
572    #[default]
573    Service,
574    #[strum(serialize = "scope")]
575    Scope,
576    #[strum(serialize = "socket")]
577    Socket,
578    #[strum(serialize = "slice")]
579    Slice,
580    #[strum(serialize = "timer")]
581    Timer,
582    #[strum(serialize = "path")]
583    Path,
584    #[strum(serialize = "target")]
585    Target,
586    #[strum(serialize = "swap")]
587    Swap,
588}
589
590/// `LoadedState` describes a Unit's current loaded state
591#[derive(Copy, Clone, PartialEq, Eq, EnumString, Debug, Default)]
592#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
593#[cfg_attr(not(feature = "serde"), derive(strum_macros::Display))]
594pub enum LoadedState {
595    #[strum(serialize = "masked", to_string = "Masked")]
596    #[default]
597    Masked,
598    #[strum(serialize = "loaded", to_string = "Loaded")]
599    Loaded,
600    #[strum(serialize = "", to_string = "Unknown")]
601    Unknown,
602}
603
604/// `ActiveState` describes a Unit's current active state
605#[derive(Copy, Clone, PartialEq, Eq, EnumString, Debug, Default)]
606#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
607#[cfg_attr(not(feature = "serde"), derive(strum_macros::Display))]
608pub enum ActiveState {
609    #[default]
610    #[strum(serialize = "inactive", to_string = "Inactive")]
611    Inactive,
612    #[strum(serialize = "active", to_string = "Active")]
613    Active,
614    #[strum(serialize = "activating", to_string = "Activating")]
615    Activating,
616    #[strum(serialize = "deactivating", to_string = "Deactivating")]
617    Deactivating,
618    #[strum(serialize = "failed", to_string = "Failed")]
619    Failed,
620    #[strum(serialize = "reloading", to_string = "Reloading")]
621    Reloading,
622    #[strum(serialize = "", to_string = "Unknown")]
623    Unknown,
624}
625
626/*
627/// Process
628#[derive(Clone, Debug)]
629pub struct Process {
630    /// pid
631    pid: u64,
632    /// command line that was executed
633    command: String,
634    /// code
635    code: String,
636    /// status
637    status: String,
638}
639
640impl Default for Process {
641    fn default() -> Process {
642        Process {
643            pid: 0,
644            command: Default::default(),
645            code: Default::default(),
646            status: Default::default(),
647        }
648    }
649}*/
650
651/// Doc describes types of documentation possibly
652/// available for a systemd `unit`
653#[derive(Clone, Debug, PartialEq)]
654#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
655pub enum Doc {
656    /// Man page is available
657    Man(String),
658    /// Webpage URL is indicated
659    Url(String),
660}
661
662impl Doc {
663    /// Unwrapps self as `Man` page
664    pub fn as_man(&self) -> Option<&str> {
665        match self {
666            Doc::Man(s) => Some(s),
667            _ => None,
668        }
669    }
670    /// Unwrapps self as webpage `Url`
671    pub fn as_url(&self) -> Option<&str> {
672        match self {
673            Doc::Url(s) => Some(s),
674            _ => None,
675        }
676    }
677}
678
679impl FromStr for Doc {
680    type Err = Error;
681    /// Builds `Doc` from systemd status descriptor
682    fn from_str(status: &str) -> Result<Self, Self::Err> {
683        let items: Vec<&str> = status.split(':').collect();
684        if items.len() != 2 {
685            return Err(Error::new(
686                ErrorKind::InvalidData,
687                "malformed doc descriptor",
688            ));
689        }
690        match items[0] {
691            "man" => {
692                let content: Vec<&str> = items[1].split('(').collect();
693                Ok(Doc::Man(content[0].to_string()))
694            },
695            "http" => Ok(Doc::Url("http:".to_owned() + items[1].trim())),
696            "https" => Ok(Doc::Url("https:".to_owned() + items[1].trim())),
697            _ => Err(Error::new(ErrorKind::InvalidData, "unknown type of doc")),
698        }
699    }
700}
701
702/// Structure to describe a systemd `unit`
703#[derive(Clone, Debug, Default, PartialEq)]
704#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
705pub struct Unit {
706    /// Unit name
707    pub name: String,
708    /// Unit type
709    pub utype: Type,
710    /// Optional unit description
711    pub description: Option<String>,
712    /// Current loaded state
713    pub loaded_state: LoadedState,
714    /// Auto start feature
715    pub auto_start: AutoStartStatus,
716    /// `true` if Self is actively running
717    pub active: bool,
718    /// `true` if this unit is auto started by default,
719    /// meaning, it should be manually disabled
720    /// not to automatically start
721    pub preset: bool,
722    /// Configuration script loaded when starting this unit
723    pub script: String,
724    /// restart policy
725    pub restart_policy: Option<String>,
726    /// optionnal killmode info
727    pub kill_mode: Option<String>,
728    /// Optionnal process description (main tasklet "name")
729    pub process: Option<String>,
730    /// Optionnal process ID number (main tasklet pid)
731    pub pid: Option<u64>,
732    /// Running task(s) infos
733    pub tasks: Option<u64>,
734    /// Optionnal CPU load consumption infos
735    pub cpu: Option<String>,
736    /// Optionnal Memory consumption infos
737    pub memory: Option<String>,
738    /// mounted partition (`What`), if this is a `mount`/`automount` unit
739    pub mounted: Option<String>,
740    /// Mount point (`Where`), if this is a `mount`/`automount` unit
741    pub mountpoint: Option<String>,
742    /// Docs / `man` page(s) available for this unit
743    pub docs: Option<Vec<Doc>>,
744    /// wants attributes: list of other service / unit names
745    pub wants: Option<Vec<String>>,
746    /// wanted_by attributes: list of other service / unit names
747    pub wanted_by: Option<Vec<String>>,
748    /// also attributes
749    pub also: Option<Vec<String>>,
750    /// `before` attributes
751    pub before: Option<Vec<String>>,
752    /// `after` attributes
753    pub after: Option<Vec<String>>,
754    /// exec_start attribute: actual command line
755    /// to be exected on `start` requests
756    pub exec_start: Option<String>,
757    /// exec_reload attribute, actual command line
758    /// to be exected on `reload` requests
759    pub exec_reload: Option<String>,
760    /// If a command is run as transient service unit, it will be started and managed
761    /// by the service manager like any other service, and thus shows up in the output
762    /// of systemctl list-units like any other unit.
763    pub transient: bool,
764}
765
766#[cfg(test)]
767mod test {
768    use super::*;
769
770    fn ctl() -> SystemCtl {
771        SystemCtl::default()
772    }
773
774    #[test]
775    fn test_status_success() {
776        let status = ctl().status("cron");
777        println!("cron status: {:#?}", status);
778        assert!(status.is_ok());
779    }
780
781    #[test]
782    fn test_status_failure() {
783        let status = ctl().status("not-existing");
784        println!("not-existing status: {:#?}", status);
785        assert!(status.is_err());
786        let result = status.map_err(|e| e.kind());
787        let expected = Err(ErrorKind::PermissionDenied);
788        assert_eq!(expected, result);
789    }
790
791    #[test]
792    fn test_is_active() {
793        let units = ["ssh", "nginx", "rsync"];
794        let ctl = ctl();
795        for u in units {
796            let active = ctl.is_active(u);
797            println!("{} is-active: {:#?}", u, active);
798            assert!(active.is_ok());
799        }
800    }
801    #[test]
802    fn test_service_exists() {
803        let units = [
804            "sshd",
805            "dropbear",
806            "ntpd",
807            "example",
808            "non-existing",
809            "dummy",
810        ];
811        let ctl = ctl();
812        for u in units {
813            let ex = ctl.exists(u);
814            println!("{} exists: {:#?}", u, ex);
815            assert!(ex.is_ok());
816        }
817    }
818    #[test]
819    fn test_disabled_services() {
820        let services = ctl().list_disabled_services().unwrap();
821        println!("disabled services: {:#?}", services)
822    }
823    #[test]
824    fn test_enabled_services() {
825        let services = ctl().list_enabled_services().unwrap();
826        println!("enabled services: {:#?}", services)
827    }
828    #[test]
829    fn test_failed_services() {
830        let services = ctl().list_failed_services().unwrap();
831        println!("failed services: {:#?}", services)
832    }
833    #[test]
834    fn test_running_services() {
835        let services = ctl().list_running_services().unwrap();
836        println!("running services: {:#?}", services)
837    }
838    #[test]
839    fn test_non_existing_unit() {
840        let unit = ctl().create_unit("non-existing");
841        assert!(unit.is_err());
842        let result = unit.map_err(|e| e.kind());
843        let expected = Err(ErrorKind::NotFound);
844        assert_eq!(expected, result);
845    }
846
847    #[test]
848    fn test_systemctl_exitcode_success() {
849        let u = ctl().create_unit("cron.service");
850        println!("{:#?}", u);
851        assert!(u.is_ok());
852    }
853
854    #[test]
855    fn test_systemctl_exitcode_not_found() {
856        let u = ctl().create_unit("cran.service");
857        println!("{:#?}", u);
858        assert!(u.is_err());
859        let result = u.map_err(|e| e.kind());
860        let expected = Err(ErrorKind::NotFound);
861        assert_eq!(expected, result);
862    }
863
864    #[test]
865    fn test_service_unit_construction() {
866        let ctl = ctl();
867        let units = ctl.list_unit_files(None, None, None).unwrap(); // all units
868        for unit in units {
869            let unit = unit.as_str();
870            if unit.contains('@') {
871                // not testing this one
872                // would require @x service # identification / enumeration
873                continue;
874            }
875            let c0 = unit.chars().next().unwrap();
876            if c0.is_alphanumeric() {
877                // valid unit name --> run test
878                let u = ctl.create_unit(unit).unwrap();
879                println!("####################################");
880                println!("Unit: {:#?}", u);
881                println!("active: {}", u.active);
882                println!("preset: {}", u.preset);
883                println!("auto_start (enabled): {:#?}", u.auto_start);
884                println!("config script : {}", u.script);
885                println!("pid: {:?}", u.pid);
886                println!("Running task(s): {:?}", u.tasks);
887                println!("Memory consumption: {:?}", u.memory);
888                println!("####################################")
889            }
890        }
891    }
892
893    #[test]
894    fn test_list_units_full() {
895        let units = ctl().list_unit_files_full(None, None, None).unwrap(); // all units
896        for unit in units {
897            println!("####################################");
898            println!("Unit: {}", unit.unit_file);
899            println!("State: {}", unit.state);
900            println!("Vendor Preset: {:?}", unit.vendor_preset);
901            println!("####################################");
902        }
903    }
904
905    /// Test valid results for the --all argument
906    /// Example of broken output:
907    /// ```text
908    /// UnitService {
909    ///     unit_name: "●",
910    ///     loaded: "syslog.service",
911    ///     state: "not-found",
912    ///     sub_state: "inactive",
913    ///     description: " dead syslog.service",
914    /// }
915    ///```
916    #[test]
917    fn test_list_units_full_all() {
918        let ctl = SystemCtl::builder()
919            .additional_args(vec![String::from("--all")])
920            .build();
921        let units = ctl.list_units_full(None, None, None).unwrap(); // all units
922        for unit in units {
923            assert_ne!("●", unit.unit_name);
924        }
925    }
926
927    #[test]
928    fn test_list_dependencies() {
929        let units = ctl().list_dependencies("sound.target").unwrap();
930        for unit in units {
931            println!("{unit}");
932        }
933    }
934
935    #[cfg(feature = "serde")]
936    #[test]
937    fn test_serde_for_unit() {
938        let mut u = Unit::default();
939        // make sure we test all enums
940        u.docs
941            .get_or_insert_with(Vec::new)
942            .push(Doc::Man("some instruction".into()));
943        u.auto_start = AutoStartStatus::Transient;
944        u.loaded_state = LoadedState::Loaded;
945        u.utype = Type::Socket;
946        // serde
947        let json_u = serde_json::to_string(&u).unwrap();
948        let reverse = serde_json::from_str(&json_u).unwrap();
949        assert_eq!(u, reverse);
950    }
951
952    #[cfg(feature = "serde")]
953    #[test]
954    fn test_serde_for_unit_list() {
955        let u = UnitList::default();
956        // serde
957        let json_u = serde_json::to_string(&u).unwrap();
958        let reverse = serde_json::from_str(&json_u).unwrap();
959        assert_eq!(u, reverse);
960    }
961}