1#![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#[derive(Builder, Default, Clone, Debug)]
25pub struct SystemCtl {
26 additional_args: Vec<String>,
28 path: Option<String>,
30}
31
32impl SystemCtl {
33 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 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 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) => {}, Some(1) => {}, Some(3) => {}, Some(4) => {
69 return Err(Error::new(
70 ErrorKind::PermissionDenied,
71 "Missing privileges or unit not found",
72 ))
73 },
74 Some(code) => {
76 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 Err(Error::new(
103 ErrorKind::UnexpectedEof,
104 "systemctl stdout empty",
105 ))
106 }
107
108 pub fn daemon_reload(&self) -> std::io::Result<ExitStatus> {
110 self.systemctl(["daemon-reload"])
111 }
112
113 pub fn restart(&self, unit: &str) -> std::io::Result<ExitStatus> {
115 self.systemctl(["restart", unit])
116 }
117
118 pub fn start(&self, unit: &str) -> std::io::Result<ExitStatus> {
120 self.systemctl(["start", unit])
121 }
122
123 pub fn stop(&self, unit: &str) -> std::io::Result<ExitStatus> {
125 self.systemctl(["stop", unit])
126 }
127
128 pub fn reload(&self, unit: &str) -> std::io::Result<ExitStatus> {
130 self.systemctl(["reload", unit])
131 }
132
133 pub fn reload_or_restart(&self, unit: &str) -> std::io::Result<ExitStatus> {
135 self.systemctl(["reload-or-restart", unit])
136 }
137
138 pub fn enable(&self, unit: &str) -> std::io::Result<ExitStatus> {
140 self.systemctl(["enable", unit])
141 }
142
143 pub fn disable(&self, unit: &str) -> std::io::Result<ExitStatus> {
145 self.systemctl(["disable", unit])
146 }
147
148 pub fn status(&self, unit: &str) -> std::io::Result<String> {
150 self.systemctl_capture(["status", unit])
151 }
152
153 pub fn cat(&self, unit: &str) -> std::io::Result<String> {
155 self.systemctl_capture(["cat", unit])
156 }
157
158 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 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 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 pub fn isolate(&self, unit: &str) -> std::io::Result<ExitStatus> {
195 self.systemctl(["isolate", unit])
196 }
197
198 pub fn freeze(&self, unit: &str) -> std::io::Result<ExitStatus> {
201 self.systemctl(["freeze", unit])
202 }
203
204 pub fn unfreeze(&self, unit: &str) -> std::io::Result<ExitStatus> {
207 self.systemctl(["thaw", unit])
208 }
209
210 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 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 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 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 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 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 pub fn list_running_services(&self) -> std::io::Result<Vec<String>> {
342 self.list_units(Some("service"), Some("running"), None)
343 }
344
345 pub fn list_failed_services(&self) -> std::io::Result<Vec<String>> {
347 self.list_units(Some("service"), Some("failed"), None)
348 }
349
350 pub fn list_disabled_services(&self) -> std::io::Result<Vec<String>> {
352 self.list_unit_files(Some("service"), Some("disabled"), None)
353 }
354
355 pub fn list_enabled_services(&self) -> std::io::Result<Vec<String>> {
357 self.list_unit_files(Some("service"), Some("enabled"), None)
358 }
359
360 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 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 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 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 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 } 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 u.mounted = Some(line.to_string())
427 } else if let Some(line) = line.strip_prefix("Where: ") {
428 u.mountpoint = Some(line.to_string());
430 } else if let Some(line) = line.strip_prefix("Main PID: ") {
431 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 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 } else if line.starts_with("CGroup: ") {
450 } else if line.starts_with("Tasks: ") {
454 } 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 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 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 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))]
514pub struct UnitList {
517 pub unit_file: String,
519 pub state: String,
521 pub vendor_preset: Option<bool>,
523}
524
525#[derive(Clone, Debug, Default, PartialEq)]
526#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
527pub struct UnitService {
530 pub unit_name: String,
532 pub loaded: LoadedState,
534 pub active: ActiveState,
536 pub sub_state: String,
538 pub description: String,
540}
541
542#[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#[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#[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#[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#[derive(Clone, Debug, PartialEq)]
654#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
655pub enum Doc {
656 Man(String),
658 Url(String),
660}
661
662impl Doc {
663 pub fn as_man(&self) -> Option<&str> {
665 match self {
666 Doc::Man(s) => Some(s),
667 _ => None,
668 }
669 }
670 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 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#[derive(Clone, Debug, Default, PartialEq)]
704#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
705pub struct Unit {
706 pub name: String,
708 pub utype: Type,
710 pub description: Option<String>,
712 pub loaded_state: LoadedState,
714 pub auto_start: AutoStartStatus,
716 pub active: bool,
718 pub preset: bool,
722 pub script: String,
724 pub restart_policy: Option<String>,
726 pub kill_mode: Option<String>,
728 pub process: Option<String>,
730 pub pid: Option<u64>,
732 pub tasks: Option<u64>,
734 pub cpu: Option<String>,
736 pub memory: Option<String>,
738 pub mounted: Option<String>,
740 pub mountpoint: Option<String>,
742 pub docs: Option<Vec<Doc>>,
744 pub wants: Option<Vec<String>>,
746 pub wanted_by: Option<Vec<String>>,
748 pub also: Option<Vec<String>>,
750 pub before: Option<Vec<String>>,
752 pub after: Option<Vec<String>>,
754 pub exec_start: Option<String>,
757 pub exec_reload: Option<String>,
760 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(); for unit in units {
869 let unit = unit.as_str();
870 if unit.contains('@') {
871 continue;
874 }
875 let c0 = unit.chars().next().unwrap();
876 if c0.is_alphanumeric() {
877 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(); 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]
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(); 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 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 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 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}