From b304a97ddbf306d6b4853a8564fddda2cc98e7f9 Mon Sep 17 00:00:00 2001 From: Jakub Koralewski <43069023+JakubKoralewski@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:21:16 +0200 Subject: [PATCH] allow passing e.g. `--user` args, don't use env-vars, refactor to struct (#31) * feat: allow passing additional args through SYSTEMCTL_ARGS e.g. `--user` args can now be passed through SYSTEMCTL_ARGS environment variable * chore: update Cargo.toml versions * refactor: struct SystemCtl instead of functions * refactor: SystemCtl struct: don't use Vecs Vecs were not necessary since Command.args() requires IntoIterator not Vec Unit required changes after refactor made tests pass to fit refactor, tests run locally * chore: bump version * style: add docs and fix clippy * Run cargo fmt Signed-off-by: Guillaume W. Bres * feat: add `daemon-reload` command support * docs: include README.md into docs doc tests are failing * docs: update README and make doc tests pass I did not have `sshd` service but I had `ssh.service` * style: cargo fmt --------- Signed-off-by: Guillaume W. Bres Co-authored-by: Guillaume W. Bres --- Cargo.toml | 9 +- README.md | 42 ++- src/lib.rs | 790 +++++++++++++++++++++++++---------------------------- 3 files changed, 401 insertions(+), 440 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5533da5..a2dfc41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "systemctl" -version = "0.3.1" +version = "0.4.0" license = "MIT OR Apache-2.0" authors = ["Guillaume W. Bres "] description = "Small crate to interact with systemd units" @@ -13,10 +13,11 @@ default = [] serde = ["dep:serde"] [dependencies] -strum = "0.25" -strum_macros = "0.25" -itertools = "0.11" +strum = "0.26" +strum_macros = "0.26" +itertools = "0.13" serde = { version = "1.0", optional = true, default-features = false, features = ["derive"] } +bon="2.3" [dev-dependencies] serde_json = "1.0" diff --git a/README.md b/README.md index 00285e3..fd5886e 100644 --- a/README.md +++ b/README.md @@ -17,28 +17,19 @@ Small rust crate to interact with systemd units Currently SystemD Version <245 are not supported as unit-file-list changed from two column to three column setup. See: [SystemD Changelog](https://github.com/systemd/systemd/blob/16bfb12c8f815a468021b6e20871061d20b50f57/NEWS#L6073) -## Environment - -`SYSTEMCTL_PATH` custom env. variable describes the absolute -location path of `systemctl` binary, by default this crate uses `/usr/bin/systemctl`, -but that can be customized: - -```shell -SYSTEMCTL_PATH=/home/$me/bin/systemctl cargo build -``` - ## Unit / service operation Nominal service operations: ```rust -systemctl::stop("systemd-journald.service") +let systemctl = systemctl::SystemCtl::default(); +systemctl.stop("systemd-journald.service") .unwrap(); -systemctl::restart("systemd-journald.service") +systemctl.restart("systemd-journald.service") .unwrap(); -if let Ok(true) = systemctl::exists("ntpd") { - let is_active = systemctl::is_active("ntpd") +if let Ok(true) = systemctl.exists("ntpd") { + let is_active = systemctl.is_active("ntpd") .unwrap(); } ``` @@ -46,20 +37,20 @@ if let Ok(true) = systemctl::exists("ntpd") { ## Service enumeration ```rust -use systemctl; +let systemctl = systemctl::SystemCtl::default(); // list all units -systemctl::list_units(None, None, None); +systemctl.list_units(None, None, None); // list all services // by adding a --type filter -systemctl::list_units(Some("service"), None, None); +systemctl.list_units(Some("service"), None, None); // list all services currently `enabled` // by adding a --state filter -systemctl::list_units(Some("service"), Some("enabled"), None); +systemctl.list_units(Some("service"), Some("enabled"), None); // list all services starting with cron -systemctl::list_units(Some("service"), None, Some("cron*")); +systemctl.list_units(Some("service"), None, Some("cron*")); ``` ## Unit structure @@ -67,9 +58,10 @@ systemctl::list_units(Some("service"), None, Some("cron*")); Use the unit structure for more information ```rust -let unit = systemctl::Unit::from_systemctl("sshd") +let systemctl = systemctl::SystemCtl::default(); +let unit = systemctl.create_unit("ssh.service") .unwrap(); -unit.restart().unwrap(); +systemctl.restart(&unit.name).unwrap(); println!("active: {}", unit.active); println!("preset: {}", unit.preset); @@ -84,11 +76,11 @@ if let Some(docs) = unit.docs { // doc pages available } } -println!("auto_start (enabled): {}", unit.auto_start); +println!("auto_start (enabled): {:?}", unit.auto_start); println!("config script : {}", unit.script); -println!("pid: {}", unit.pid); -println!("Running task(s): {}", unit.tasks.unwrap()); -println!("Memory consumption: {}", unit.memory.unwrap()); +println!("pid: {:?}", unit.pid); +println!("Running task(s): {:?}", unit.tasks); +println!("Memory consumption: {:?}", unit.memory); ``` ## TODO diff --git a/src/lib.rs b/src/lib.rs index c86688c..393ea72 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ //! Crate to manage and monitor services through `systemctl` //! Homepage: +#![doc=include_str!("../README.md")] use std::io::{Error, ErrorKind, Read}; use std::process::{Child, ExitStatus}; use std::str::FromStr; @@ -10,189 +11,387 @@ use serde::{Deserialize, Serialize}; const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl"; -/// Invokes `systemctl $args` -fn spawn_child(args: Vec<&str>) -> std::io::Result { - std::process::Command::new(std::env::var("SYSTEMCTL_PATH").unwrap_or(SYSTEMCTL_PATH.into())) - .args(args) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::null()) - .spawn() -} +use bon::Builder; + +/// Struct with API calls to systemctl. +/// +/// Use the `::default()` impl if you don't need special arguments. +/// +/// Use the builder API when you want to specify a custom path to systemctl binary or extra args. +#[derive(Builder, Default, Clone, Debug)] +pub struct SystemCtl { + /// Allows passing global arguments to systemctl like `--user`. + additional_args: Vec, + /// The path to the systemctl binary, by default it's [SYSTEMCTL_PATH] + path: Option, +} + +impl SystemCtl { + /// Invokes `systemctl $args` + fn spawn_child<'a, 's: 'a, S: IntoIterator>( + &'s self, + args: S, + ) -> std::io::Result { + std::process::Command::new(self.get_path()) + .args(self.additional_args.iter().map(String::as_str).chain(args)) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::null()) + .spawn() + } + + fn get_path(&self) -> &str { + self.path.as_deref().unwrap_or(SYSTEMCTL_PATH) + } + + /// Invokes `systemctl $args` silently + fn systemctl<'a, 's: 'a, S: IntoIterator>( + &'s self, + args: S, + ) -> std::io::Result { + self.spawn_child(args)?.wait() + } + + /// Invokes `systemctl $args` and captures stdout stream + fn systemctl_capture<'a, 's: 'a, S: IntoIterator>( + &'s self, + args: S, + ) -> std::io::Result { + let mut child = self.spawn_child(args)?; + match child.wait()?.code() { + Some(0) => {}, // success + Some(1) => {}, // success -> Ok(Unit not found) + Some(3) => {}, // success -> Ok(unit is inactive and/or dead) + Some(4) => { + return Err(Error::new( + ErrorKind::PermissionDenied, + "Missing Priviledges or Unit not found", + )) + }, + // unknown errorcodes + Some(code) => { + return Err(Error::new( + // TODO: Maybe a better ErrorKind, none really seem to fit + ErrorKind::Other, + format!("Process exited with code: {code}"), + )); + }, + None => { + return Err(Error::new( + ErrorKind::Interrupted, + "Process terminated by signal", + )) + }, + } -/// Invokes `systemctl $args` silently -fn systemctl(args: Vec<&str>) -> std::io::Result { - spawn_child(args)?.wait() -} + let mut stdout: Vec = Vec::new(); + let size = child.stdout.unwrap().read_to_end(&mut stdout)?; -/// Invokes `systemctl $args` and captures stdout stream -fn systemctl_capture(args: Vec<&str>) -> std::io::Result { - let mut child = spawn_child(args)?; - match child.wait()?.code() { - Some(code) if code == 0 => {}, // success - Some(code) if code == 1 => {}, // success -> Ok(Unit not found) - Some(code) if code == 3 => {}, // success -> Ok(unit is inactive and/or dead) - Some(code) if code == 4 => { - return Err(Error::new( - ErrorKind::PermissionDenied, - "Missing Priviledges or Unit not found", - )) - }, - // unknown errorcodes - Some(code) => { - return Err(Error::new( - // TODO: Maybe a better ErrorKind, none really seem to fit - ErrorKind::Other, - format!("Process exited with code: {code}"), - )); - }, - None => { - return Err(Error::new( - ErrorKind::Interrupted, - "Process terminated by signal", - )) - }, + if size > 0 { + if let Ok(s) = String::from_utf8(stdout) { + return Ok(s); + } else { + return Err(Error::new( + ErrorKind::InvalidData, + "Invalid utf8 data in stdout", + )); + } + } + + // if this is reached all if's above did not work + Err(Error::new( + ErrorKind::UnexpectedEof, + "systemctl stdout empty", + )) } - let mut stdout: Vec = Vec::new(); - let size = child.stdout.unwrap().read_to_end(&mut stdout)?; + /// Reloads all unit files + pub fn daemon_reload(&self) -> std::io::Result { + self.systemctl(["daemon-reload"]) + } - if size > 0 { - if let Ok(s) = String::from_utf8(stdout) { - return Ok(s); - } else { - return Err(Error::new( - ErrorKind::InvalidData, - "Invalid utf8 data in stdout", - )); - } + /// Forces given `unit` to (re)start + pub fn restart(&self, unit: &str) -> std::io::Result { + self.systemctl(["restart", unit]) } - // if this is reached all if's above did not work - Err(Error::new( - ErrorKind::UnexpectedEof, - "systemctl stdout empty", - )) -} + /// Forces given `unit` to start + pub fn start(&self, unit: &str) -> std::io::Result { + self.systemctl(["start", unit]) + } -/// Forces given `unit` to (re)start -pub fn restart(unit: &str) -> std::io::Result { - systemctl(vec!["restart", unit]) -} + /// Forces given `unit` to stop + pub fn stop(&self, unit: &str) -> std::io::Result { + self.systemctl(["stop", unit]) + } -/// Forces given `unit` to start -pub fn start(unit: &str) -> std::io::Result { - systemctl(vec!["start", unit]) -} + /// Triggers reload for given `unit` + pub fn reload(&self, unit: &str) -> std::io::Result { + self.systemctl(["reload", unit]) + } -/// Forces given `unit` to stop -pub fn stop(unit: &str) -> std::io::Result { - systemctl(vec!["stop", unit]) -} + /// Triggers reload or restarts given `unit` + pub fn reload_or_restart(&self, unit: &str) -> std::io::Result { + self.systemctl(["reload-or-restart", unit]) + } -/// Triggers reload for given `unit` -pub fn reload(unit: &str) -> std::io::Result { - systemctl(vec!["reload", unit]) -} + /// Enable given `unit` to start at boot + pub fn enable(&self, unit: &str) -> std::io::Result { + self.systemctl(["enable", unit]) + } -/// Triggers reload or restarts given `unit` -pub fn reload_or_restart(unit: &str) -> std::io::Result { - systemctl(vec!["reload-or-restart", unit]) -} + /// Disable given `unit` to start at boot + pub fn disable(&self, unit: &str) -> std::io::Result { + self.systemctl(["disable", unit]) + } -/// Enable given `unit` to start at boot -pub fn enable(unit: &str) -> std::io::Result { - systemctl(vec!["enable", unit]) -} + /// Returns raw status from `systemctl status $unit` call + pub fn status(&self, unit: &str) -> std::io::Result { + self.systemctl_capture(["status", unit]) + } -/// Disable given `unit` to start at boot -pub fn disable(unit: &str) -> std::io::Result { - systemctl(vec!["disable", unit]) -} + /// Invokes systemctl `cat` on given `unit` + pub fn cat(&self, unit: &str) -> std::io::Result { + self.systemctl_capture(["cat", unit]) + } -/// Returns raw status from `systemctl status $unit` call -pub fn status(unit: &str) -> std::io::Result { - systemctl_capture(vec!["status", unit]) -} + /// Returns `true` if given `unit` is actively running + pub fn is_active(&self, unit: &str) -> std::io::Result { + let status = self.systemctl_capture(["is-active", unit])?; + Ok(status.trim_end().eq("active")) + } -/// Invokes systemctl `cat` on given `unit` -pub fn cat(unit: &str) -> std::io::Result { - systemctl_capture(vec!["cat", unit]) -} + /// Isolates given unit, only self and its dependencies are + /// now actively running + pub fn isolate(&self, unit: &str) -> std::io::Result { + self.systemctl(["isolate", unit]) + } -/// Returns `true` if given `unit` is actively running -pub fn is_active(unit: &str) -> std::io::Result { - let status = systemctl_capture(vec!["is-active", unit])?; - Ok(status.trim_end().eq("active")) -} + /// Freezes (halts) given unit. + /// This operation might not be feasible. + pub fn freeze(&self, unit: &str) -> std::io::Result { + self.systemctl(["freeze", unit]) + } -/// Isolates given unit, only self and its dependencies are -/// now actively running -pub fn isolate(unit: &str) -> std::io::Result { - systemctl(vec!["isolate", unit]) -} + /// Unfreezes given unit (recover from halted state). + /// This operation might not be feasible. + pub fn unfreeze(&self, unit: &str) -> std::io::Result { + self.systemctl(["thaw", unit]) + } -/// Freezes (halts) given unit. -/// This operation might not be feasible. -pub fn freeze(unit: &str) -> std::io::Result { - systemctl(vec!["freeze", unit]) -} + /// Returns `true` if given `unit` exists, + /// ie., service could be or is actively deployed + /// and manageable by systemd + pub fn exists(&self, unit: &str) -> std::io::Result { + let unit_list = self.list_units(None, None, Some(unit))?; + Ok(!unit_list.is_empty()) + } + + /// Returns a `Vector` of `UnitList` structs extracted from systemctl listing. + /// + type filter: optional `--type` filter + /// + state filter: optional `--state` filter + /// + glob filter: optional unit name filter + pub fn list_units_full( + &self, + type_filter: Option<&str>, + state_filter: Option<&str>, + glob: Option<&str>, + ) -> std::io::Result> { + let mut args = vec!["list-unit-files"]; + if let Some(filter) = type_filter { + args.push("--type"); + args.push(filter) + } + if let Some(filter) = state_filter { + args.push("--state"); + args.push(filter) + } + if let Some(glob) = glob { + args.push(glob) + } + let mut result: Vec = Vec::new(); + let content = self.systemctl_capture(args)?; + let lines = content + .lines() + .filter(|line| line.contains('.') && !line.ends_with('.')); + + for l in lines { + let parsed: Vec<&str> = l.split_ascii_whitespace().collect(); + let vendor_preset = match parsed[2] { + "-" => None, + "enabled" => Some(true), + "disabled" => Some(false), + _ => None, + }; + result.push(UnitList { + unit_file: parsed[0].to_string(), + state: parsed[1].to_string(), + vendor_preset, + }) + } + Ok(result) + } -/// Unfreezes given unit (recover from halted state). -/// This operation might not be feasible. -pub fn unfreeze(unit: &str) -> std::io::Result { - systemctl(vec!["thaw", unit]) -} + /// Returns a `Vector` of unit names extracted from systemctl listing. + /// + type filter: optional `--type` filter + /// + state filter: optional `--state` filter + /// + glob filter: optional unit name filter + pub fn list_units( + &self, + type_filter: Option<&str>, + state_filter: Option<&str>, + glob: Option<&str>, + ) -> std::io::Result> { + let list = self.list_units_full(type_filter, state_filter, glob); + Ok(list?.iter().map(|n| n.unit_file.clone()).collect()) + } -/// Returns `true` if given `unit` exists, -/// ie., service could be or is actively deployed -/// and manageable by systemd -pub fn exists(unit: &str) -> std::io::Result { - let unit_list = list_units(None, None, Some(unit))?; - Ok(!unit_list.is_empty()) -} + /// Returns list of services that are currently declared as disabled + pub fn list_disabled_services(&self) -> std::io::Result> { + self.list_units(Some("service"), Some("disabled"), None) + } -/// Returns a `Vector` of `UnitList` structs extracted from systemctl listing. -/// + type filter: optional `--type` filter -/// + state filter: optional `--state` filter -/// + glob filter: optional unit name filter -pub fn list_units_full( - type_filter: Option<&str>, - state_filter: Option<&str>, - glob: Option<&str>, -) -> std::io::Result> { - let mut args = vec!["list-unit-files"]; - if let Some(filter) = type_filter { - args.push("--type"); - args.push(filter) - } - if let Some(filter) = state_filter { - args.push("--state"); - args.push(filter) - } - if let Some(glob) = glob { - args.push(glob) - } - let mut result: Vec = Vec::new(); - let content = systemctl_capture(args)?; - let lines = content - .lines() - .filter(|line| line.contains('.') && !line.ends_with('.')); - - for l in lines { - let parsed: Vec<&str> = l.split_ascii_whitespace().collect(); - let vendor_preset = match parsed[2] { - "-" => None, - "enabled" => Some(true), - "disabled" => Some(false), - _ => None, + /// Returns list of services that are currently declared as enabled + pub fn list_enabled_services(&self) -> std::io::Result> { + self.list_units(Some("service"), Some("enabled"), None) + } + + /// Builds a new `Unit` structure by retrieving + /// structure attributes with a `systemctl status $unit` call + pub fn create_unit(&self, name: &str) -> std::io::Result { + if let Ok(false) = self.exists(name) { + return Err(Error::new( + ErrorKind::NotFound, + format!("Unit or service \"{}\" does not exist", name), + )); + } + let mut u = Unit::default(); + let status = self.status(name)?; + let mut lines = status.lines(); + let next = lines.next().unwrap(); + let (_, rem) = next.split_at(3); + let mut items = rem.split_ascii_whitespace(); + let name_raw = items.next().unwrap().trim(); + if let Some(delim) = items.next() { + if delim.trim().eq("-") { + // --> description string is provided + let items: Vec<_> = items.collect(); + u.description = Some(itertools::join(&items, " ")); + } + } + let (name, utype_raw) = name_raw + .rsplit_once('.') + .expect("Unit is missing a Type, this should not happen!"); + // `type` is deduced from .extension + u.utype = match Type::from_str(utype_raw) { + Ok(t) => t, + Err(e) => panic!("For {:?} -> {e}", name_raw), }; - result.push(UnitList { - unit_file: parsed[0].to_string(), - state: parsed[1].to_string(), - vendor_preset, - }) + let mut is_doc = false; + for line in lines { + let line = line.trim_start(); + if let Some(line) = line.strip_prefix("Loaded: ") { + // Match and get rid of "Loaded: " + if let Some(line) = line.strip_prefix("loaded ") { + u.state = State::Loaded; + let line = line.strip_prefix('(').unwrap(); + let line = line.strip_suffix(')').unwrap(); + let items: Vec<&str> = line.split(';').collect(); + u.script = items[0].trim().to_string(); + u.auto_start = match AutoStartStatus::from_str(items[1].trim()) { + Ok(x) => x, + Err(_) => AutoStartStatus::Disabled, + }; + if items.len() > 2 { + // preset is optionnal ? + u.preset = items[2].trim().ends_with("enabled"); + } + } else if line.starts_with("masked") { + u.state = State::Masked; + } + } else if let Some(line) = line.strip_prefix("Transient: ") { + if line == "yes" { + u.transient = true + } + } else if line.starts_with("Active: ") { + // skip that one + // we already have .active() .inative() methods + // to access this information + } else if let Some(line) = line.strip_prefix("Docs: ") { + is_doc = true; + if let Ok(doc) = Doc::from_str(line) { + u.docs.get_or_insert_with(Vec::new).push(doc); + } + } else if let Some(line) = line.strip_prefix("What: ") { + // mountpoint infos + u.mounted = Some(line.to_string()) + } else if let Some(line) = line.strip_prefix("Where: ") { + // mountpoint infos + u.mountpoint = Some(line.to_string()); + } else if let Some(line) = line.strip_prefix("Main PID: ") { + // example -> Main PID: 787 (gpm) + if let Some((pid, proc)) = line.split_once(' ') { + u.pid = Some(pid.parse::().unwrap_or(0)); + u.process = Some(proc.replace(&['(', ')'][..], "")); + }; + } else if let Some(line) = line.strip_prefix("Cntrl PID: ") { + // example -> Main PID: 787 (gpm) + if let Some((pid, proc)) = line.split_once(' ') { + u.pid = Some(pid.parse::().unwrap_or(0)); + u.process = Some(proc.replace(&['(', ')'][..], "")); + }; + } else if line.starts_with("Process: ") { + //TODO: implement + //TODO: parse as a Process item + //let items : Vec<_> = line.split_ascii_whitespace().collect(); + //let proc_pid = u64::from_str_radix(items[1].trim(), 10).unwrap(); + //let cli; + //Process: 640 ExecStartPre=/usr/sbin/sshd -t (code=exited, status=0/SUCCESS) + } else if line.starts_with("CGroup: ") { + //TODO: implement + //LINE: "CGroup: /system.slice/sshd.service" + //LINE: "└─1050 /usr/sbin/sshd -D" + } else if line.starts_with("Tasks: ") { + //TODO: implement + } else if let Some(line) = line.strip_prefix("Memory: ") { + u.memory = Some(line.trim().to_string()); + } else if let Some(line) = line.strip_prefix("CPU: ") { + u.cpu = Some(line.trim().to_string()) + } else { + // handling multi line cases + if is_doc { + let line = line.trim_start(); + if let Ok(doc) = Doc::from_str(line) { + u.docs.get_or_insert_with(Vec::new).push(doc); + } + } + } + } + + if let Ok(content) = self.cat(name) { + let line_tuple = content + .lines() + .filter_map(|line| line.split_once('=').to_owned()); + for (k, v) in line_tuple { + let val = v.to_string(); + match k { + "Wants" => u.wants.get_or_insert_with(Vec::new).push(val), + "WantedBy" => u.wanted_by.get_or_insert_with(Vec::new).push(val), + "Also" => u.also.get_or_insert_with(Vec::new).push(val), + "Before" => u.before.get_or_insert_with(Vec::new).push(val), + "After" => u.after.get_or_insert_with(Vec::new).push(val), + "ExecStart" => u.exec_start = Some(val), + "ExecReload" => u.exec_reload = Some(val), + "Restart" => u.restart_policy = Some(val), + "KillMode" => u.kill_mode = Some(val), + _ => {}, + } + } + } + + u.active = self.is_active(name)?; + u.name = name.to_string(); + Ok(u) } - Ok(result) } #[derive(Clone, Debug, Default, PartialEq)] @@ -208,29 +407,6 @@ pub struct UnitList { pub vendor_preset: Option, } -/// Returns a `Vector` of unit names extracted from systemctl listing. -/// + type filter: optional `--type` filter -/// + state filter: optional `--state` filter -/// + glob filter: optional unit name filter -pub fn list_units( - type_filter: Option<&str>, - state_filter: Option<&str>, - glob: Option<&str>, -) -> std::io::Result> { - let list = list_units_full(type_filter, state_filter, glob); - Ok(list?.iter().map(|n| n.unit_file.clone()).collect()) -} - -/// Returns list of services that are currently declared as disabled -pub fn list_disabled_services() -> std::io::Result> { - list_units(Some("service"), Some("disabled"), None) -} - -/// Returns list of services that are currently declared as enabled -pub fn list_enabled_services() -> std::io::Result> { - list_units(Some("service"), Some("enabled"), None) -} - /// `AutoStartStatus` describes the Unit current state #[derive(Copy, Clone, PartialEq, Eq, EnumString, Debug, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -431,235 +607,24 @@ pub struct Unit { pub transient: bool, } -// TODO: Remove this lint fix -#[allow(clippy::if_same_then_else)] -impl Unit { - /// Builds a new `Unit` structure by retrieving - /// structure attributes with a `systemctl status $unit` call - pub fn from_systemctl(name: &str) -> std::io::Result { - if let Ok(false) = exists(name) { - return Err(Error::new( - ErrorKind::NotFound, - format!("Unit or service \"{}\" does not exist", name), - )); - } - let mut u = Unit::default(); - let status = status(name)?; - let mut lines = status.lines(); - let next = lines.next().unwrap(); - let (_, rem) = next.split_at(3); - let mut items = rem.split_ascii_whitespace(); - let name_raw = items.next().unwrap().trim(); - if let Some(delim) = items.next() { - if delim.trim().eq("-") { - // --> description string is provided - let items: Vec<_> = items.collect(); - u.description = Some(itertools::join(&items, " ")); - } - } - let (name, utype_raw) = name_raw - .rsplit_once('.') - .expect("Unit is missing a Type, this should not happen!"); - // `type` is deduced from .extension - u.utype = match Type::from_str(utype_raw) { - Ok(t) => t, - Err(e) => panic!("For {:?} -> {e}", name_raw), - }; - let mut is_doc = false; - for line in lines { - let line = line.trim_start(); - if let Some(line) = line.strip_prefix("Loaded: ") { - // Match and get rid of "Loaded: " - if let Some(line) = line.strip_prefix("loaded ") { - u.state = State::Loaded; - let line = line.strip_prefix('(').unwrap(); - let line = line.strip_suffix(')').unwrap(); - let items: Vec<&str> = line.split(';').collect(); - u.script = items[0].trim().to_string(); - u.auto_start = match AutoStartStatus::from_str(items[1].trim()) { - Ok(x) => x, - Err(_) => AutoStartStatus::Disabled, - }; - if items.len() > 2 { - // preset is optionnal ? - u.preset = items[2].trim().ends_with("enabled"); - } - } else if line.starts_with("masked") { - u.state = State::Masked; - } - } else if let Some(line) = line.strip_prefix("Transient: ") { - if line == "yes" { - u.transient = true - } - } else if line.starts_with("Active: ") { - // skip that one - // we already have .active() .inative() methods - // to access this information - } else if let Some(line) = line.strip_prefix("Docs: ") { - is_doc = true; - if let Ok(doc) = Doc::from_str(line) { - u.docs.get_or_insert_with(Vec::new).push(doc); - } - } else if let Some(line) = line.strip_prefix("What: ") { - // mountpoint infos - u.mounted = Some(line.to_string()) - } else if let Some(line) = line.strip_prefix("Where: ") { - // mountpoint infos - u.mountpoint = Some(line.to_string()); - } else if let Some(line) = line.strip_prefix("Main PID: ") { - // example -> Main PID: 787 (gpm) - if let Some((pid, proc)) = line.split_once(' ') { - u.pid = Some(pid.parse::().unwrap_or(0)); - u.process = Some(proc.replace(&['(', ')'][..], "")); - }; - } else if let Some(line) = line.strip_prefix("Cntrl PID: ") { - // example -> Main PID: 787 (gpm) - if let Some((pid, proc)) = line.split_once(' ') { - u.pid = Some(pid.parse::().unwrap_or(0)); - u.process = Some(proc.replace(&['(', ')'][..], "")); - }; - } else if line.starts_with("Process: ") { - //TODO: implement - //TODO: parse as a Process item - //let items : Vec<_> = line.split_ascii_whitespace().collect(); - //let proc_pid = u64::from_str_radix(items[1].trim(), 10).unwrap(); - //let cli; - //Process: 640 ExecStartPre=/usr/sbin/sshd -t (code=exited, status=0/SUCCESS) - } else if line.starts_with("CGroup: ") { - //TODO: implement - //LINE: "CGroup: /system.slice/sshd.service" - //LINE: "└─1050 /usr/sbin/sshd -D" - } else if line.starts_with("Tasks: ") { - //TODO: implement - } else if let Some(line) = line.strip_prefix("Memory: ") { - u.memory = Some(line.trim().to_string()); - } else if let Some(line) = line.strip_prefix("CPU: ") { - u.cpu = Some(line.trim().to_string()) - } else { - // handling multi line cases - if is_doc { - let line = line.trim_start(); - if let Ok(doc) = Doc::from_str(line) { - u.docs.get_or_insert_with(Vec::new).push(doc); - } - } - } - } - - if let Ok(content) = cat(name) { - let line_tuple = content - .lines() - .filter_map(|line| line.split_once('=').to_owned()); - for (k, v) in line_tuple { - let val = v.to_string(); - match k { - "Wants" => u.wants.get_or_insert_with(Vec::new).push(val), - "WantedBy" => u.wanted_by.get_or_insert_with(Vec::new).push(val), - "Also" => u.also.get_or_insert_with(Vec::new).push(val), - "Before" => u.before.get_or_insert_with(Vec::new).push(val), - "After" => u.after.get_or_insert_with(Vec::new).push(val), - "ExecStart" => u.exec_start = Some(val), - "ExecReload" => u.exec_reload = Some(val), - "Restart" => u.restart_policy = Some(val), - "KillMode" => u.kill_mode = Some(val), - _ => {}, - } - // } - } - } - - u.active = is_active(name)?; - u.name = name.to_string(); - Ok(u) - } - - /// Restarts Self by invoking `systemctl` - pub fn restart(&self) -> std::io::Result { - restart(&self.name) - } - - /// Starts Self by invoking `systemctl` - pub fn start(&self) -> std::io::Result { - start(&self.name) - } - - /// Stops Self by invoking `systemctl` - pub fn stop(&self) -> std::io::Result { - stop(&self.name) - } - - /// Reloads Self by invoking systemctl - pub fn reload(&self) -> std::io::Result { - reload(&self.name) - } - - /// Reloads or restarts Self by invoking systemctl - pub fn reload_or_restart(&self) -> std::io::Result { - reload_or_restart(&self.name) - } - - /// Enable Self to start at boot - pub fn enable(&self) -> std::io::Result { - enable(&self.name) - } - - /// Disable Self to start at boot - pub fn disable(&self) -> std::io::Result { - disable(&self.name) - } - - /// Returns verbose status for Self - pub fn status(&self) -> std::io::Result { - status(&self.name) - } - - /// Returns `true` if Self is actively running - pub fn is_active(&self) -> std::io::Result { - is_active(&self.name) - } - - /// `Isolate` Self, meaning stops all other units but - /// self and its dependencies - pub fn isolate(&self) -> std::io::Result { - isolate(&self.name) - } - - /// `Freezes` Self, halts self and CPU load will - /// no longer be dedicated to its execution. - /// This operation might not be feasible. - /// `unfreeze()` is the mirror operation - pub fn freeze(&self) -> std::io::Result { - freeze(&self.name) - } - - /// `Unfreezes` Self, exists halted state. - /// This operation might not be feasible. - pub fn unfreeze(&self) -> std::io::Result { - unfreeze(&self.name) - } - - /// Returns `true` if given `unit` exists, - /// ie., service could be or is actively deployed - /// and manageable by systemd - pub fn exists(&self) -> std::io::Result { - exists(&self.name) - } -} - #[cfg(test)] mod test { use super::*; + fn ctl() -> SystemCtl { + SystemCtl::default() + } + #[test] fn test_status_success() { - let status = status("cron"); + let status = ctl().status("cron"); println!("cron status: {:#?}", status); assert!(status.is_ok()); } #[test] fn test_status_failure() { - let status = status("not-existing"); + let status = ctl().status("not-existing"); println!("not-existing status: {:#?}", status); assert!(status.is_err()); let result = status.map_err(|e| e.kind()); @@ -669,16 +634,17 @@ mod test { #[test] fn test_is_active() { - let units = vec!["sshd", "dropbear", "ntpd"]; + let units = ["sshd", "dropbear", "ntpd"]; + let ctl = ctl(); for u in units { - let active = is_active(u); + let active = ctl.is_active(u); println!("{} is-active: {:#?}", u, active); assert!(active.is_ok()); } } #[test] fn test_service_exists() { - let units = vec![ + let units = [ "sshd", "dropbear", "ntpd", @@ -686,25 +652,26 @@ mod test { "non-existing", "dummy", ]; + let ctl = ctl(); for u in units { - let ex = exists(u); + let ex = ctl.exists(u); println!("{} exists: {:#?}", u, ex); assert!(ex.is_ok()); } } #[test] fn test_disabled_services() { - let services = list_disabled_services().unwrap(); + let services = ctl().list_disabled_services().unwrap(); println!("disabled services: {:#?}", services) } #[test] fn test_enabled_services() { - let services = list_enabled_services().unwrap(); + let services = ctl().list_enabled_services().unwrap(); println!("enabled services: {:#?}", services) } #[test] fn test_non_existing_unit() { - let unit = Unit::from_systemctl("non-existing"); + let unit = ctl().create_unit("non-existing"); assert!(unit.is_err()); let result = unit.map_err(|e| e.kind()); let expected = Err(ErrorKind::NotFound); @@ -713,14 +680,14 @@ mod test { #[test] fn test_systemctl_exitcode_success() { - let u = Unit::from_systemctl("cron.service"); + let u = ctl().create_unit("cron.service"); println!("{:#?}", u); assert!(u.is_ok()); } #[test] fn test_systemctl_exitcode_not_found() { - let u = Unit::from_systemctl("cran.service"); + let u = ctl().create_unit("cran.service"); println!("{:#?}", u); assert!(u.is_err()); let result = u.map_err(|e| e.kind()); @@ -730,7 +697,8 @@ mod test { #[test] fn test_service_unit_construction() { - let units = list_units(None, None, None).unwrap(); // all units + let ctl = ctl(); + let units = ctl.list_units(None, None, None).unwrap(); // all units for unit in units { let unit = unit.as_str(); if unit.contains('@') { @@ -741,7 +709,7 @@ mod test { let c0 = unit.chars().next().unwrap(); if c0.is_alphanumeric() { // valid unit name --> run test - let u = Unit::from_systemctl(unit).unwrap(); + let u = ctl.create_unit(unit).unwrap(); println!("####################################"); println!("Unit: {:#?}", u); println!("active: {}", u.active); @@ -757,7 +725,7 @@ mod test { } #[test] fn test_list_units_full() { - let units = list_units_full(None, None, None).unwrap(); // all units + let units = ctl().list_units_full(None, None, None).unwrap(); // all units for unit in units { println!("####################################"); println!("Unit: {}", unit.unit_file);