diff options
Diffstat (limited to 'crates/core/tedge/src/system_services')
10 files changed, 586 insertions, 711 deletions
diff --git a/crates/core/tedge/src/system_services/command_builder.rs b/crates/core/tedge/src/system_services/command_builder.rs index 9d9e8ffb..bb66f313 100644 --- a/crates/core/tedge/src/system_services/command_builder.rs +++ b/crates/core/tedge/src/system_services/command_builder.rs @@ -13,8 +13,12 @@ impl CommandBuilder { } } - pub fn arg(mut self, arg: impl AsRef<OsStr>) -> CommandBuilder { - self.command.arg(arg); + pub fn args<I, S>(mut self, args: I) -> CommandBuilder + where + I: IntoIterator<Item = S>, + S: AsRef<OsStr>, + { + self.command.args(args); self } diff --git a/crates/core/tedge/src/system_services/error.rs b/crates/core/tedge/src/system_services/error.rs index 1b2e8c1a..0de341b3 100644 --- a/crates/core/tedge/src/system_services/error.rs +++ b/crates/core/tedge/src/system_services/error.rs @@ -1,63 +1,34 @@ #[derive(thiserror::Error, Debug)] pub enum SystemServiceError { - #[error(transparent)] - IoError(#[from] std::io::Error), - - #[error(transparent)] - SystemdError(#[from] SystemdError), - - #[error(transparent)] - OpenRcServiceError(#[from] OpenRcServiceError), - - #[error(transparent)] - BsdServiceError(#[from] BsdServiceError), - - #[error("Unexpected value for exit status.")] - UnexpectedExitStatus, - - #[error("Unsupported operation.")] - UnsupportedOperation, - - #[error("Service Manager: '{0}' is not available on the system or elevated permissions have not been granted.")] - ServiceManagerUnavailable(String), -} - -/// The error type used by the `SystemdServiceManager` -#[derive(thiserror::Error, Debug)] -pub enum SystemdError { - #[error("Systemd returned unspecific error for service {service} while performing {cmd} it.\nHint: {hint}")] - UnspecificError { - service: &'static str, - cmd: &'static str, - hint: &'static str, - }, - - #[error("Service {service} not found. Install {service} to use this command.")] - ServiceNotFound { service: &'static str }, + #[error("Service command <{service_command:?}> failed with code: {code:?}.")] + ServiceCommandFailedWithCode { service_command: String, code: i32 }, - #[error("Service {service} not loaded.")] - ServiceNotLoaded { service: &'static str }, + #[error("Service command <{service_command:?}> terminated by a signal.")] + ServiceCommandFailedBySignal { service_command: String }, - #[error("Returned exit code: '{code:?}' for: systemd' is unhandled.")] - UnhandledReturnCode { code: i32 }, -} - -/// The error type used by the `OpenRcServiceManager` -#[derive(thiserror::Error, Debug)] -pub enum OpenRcServiceError { - #[error("Service command <{service_command:?}> failed with code: {code:?}.")] - ServiceCommandFailed { + #[error( + "Service command <{service_command:?}> not found.\n\ + Check '{path}' file." + )] + ServiceCommandNotFound { service_command: String, - code: Option<i32>, + path: String, }, -} -/// The error type used by the `BsdServiceManager` -#[derive(thiserror::Error, Debug)] -pub enum BsdServiceError { - #[error("Service command <{service_command:?}> failed with code: {code:?}.")] - ServiceCommandFailed { - service_command: String, - code: Option<i32>, + #[error("Failed to execute '{cmd}' to check the service manager availability.\n\ + Service manager '{name}' is not available on the system or elevated permissions have not been granted.")] + ServiceManagerUnavailable { cmd: String, name: String }, + + #[error("Toml syntax error in the system config file '{path}': {reason}")] + SystemConfigInvalidToml { path: String, reason: String }, + + #[error( + "Syntax error in the system config file for '{cmd}': {reason}\n\ + Check '{path}' file." + )] + SystemConfigInvalidSyntax { + reason: String, + cmd: String, + path: String, }, } diff --git a/crates/core/tedge/src/system_services/manager.rs b/crates/core/tedge/src/system_services/manager.rs index 899f6386..7c39ea8c 100644 --- a/crates/core/tedge/src/system_services/manager.rs +++ b/crates/core/tedge/src/system_services/manager.rs @@ -1,5 +1,8 @@ use crate::system_services::*; use std::fmt::Debug; +use std::path::PathBuf; +use std::sync::Arc; +use tedge_users::UserManager; /// Abstraction over the system-provided facility that manages starting, stopping as well as other /// service-related management functions of system services. @@ -38,3 +41,13 @@ pub trait SystemServiceManager: Debug { } } } + +pub fn service_manager( + user_manager: UserManager, + config_root: PathBuf, +) -> Result<Arc<dyn SystemServiceManager>, SystemServiceError> { + Ok(Arc::new(GeneralServiceManager::try_new( + user_manager, + config_root, + )?)) +} diff --git a/crates/core/tedge/src/system_services/managers/bsd.rs b/crates/core/tedge/src/system_services/managers/bsd.rs deleted file mode 100644 index bca56a1c..00000000 --- a/crates/core/tedge/src/system_services/managers/bsd.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::system_services::*; -use std::process::ExitStatus; -use tedge_users::{UserManager, ROOT_USER}; - -/// Service manager that uses `service(8)` as found on FreeBSD to control system services. -/// -#[derive(Debug)] -pub struct BsdServiceManager { - user_manager: UserManager, -} - -impl BsdServiceManager { - pub fn new(user_manager: UserManager) -> Self { - Self { user_manager } - } -} - -impl SystemServiceManager for BsdServiceManager { - fn name(&self) -> &str { - "service(8)" - } - - fn check_operational(&self) -> Result<(), SystemServiceError> { - let mut command = ServiceCommand::CheckManager.into_command(); - - match command.status() { - Ok(status) if status.success() => Ok(()), - _ => Err(SystemServiceError::ServiceManagerUnavailable( - self.name().to_string(), - )), - } - } - - fn stop_service(&self, service: SystemService) -> Result<(), SystemServiceError> { - let service_command = ServiceCommand::Stop(service); - - self.run_service_command_as_root(service_command)? - .must_succeed() - } - - fn restart_service(&self, service: SystemService) -> Result<(), SystemServiceError> { - let service_command = ServiceCommand::Restart(service); - - self.run_service_command_as_root(service_command)? - .must_succeed() - } - - fn enable_service(&self, service: SystemService) -> Result<(), SystemServiceError> { - let service_command = ServiceCommand::Enable(service); - - self.run_service_command_as_root(service_command)? - .must_succeed() - } - - fn disable_service(&self, service: SystemService) -> Result<(), SystemServiceError> { - let service_command = ServiceCommand::Disable(service); - - self.run_service_command_as_root(service_command)? - .must_succeed() - } - - fn is_service_running(&self, service: SystemService) -> Result<bool, SystemServiceError> { - let service_command = ServiceCommand::IsActive(service); - - self.run_service_command_as_root(service_command) - .map(|status| status.success()) - } -} - -impl BsdServiceManager { - fn run_service_command_as_root( - &self, - service_command: ServiceCommand, - ) -> Result<ServiceCommandExitStatus, SystemServiceError> { - let _root_guard = self.user_manager.become_user(ROOT_USER); - - service_command - .into_command() - .status() - .map_err(Into::into) - .map(|status| ServiceCommandExitStatus { - status, - service_command, - }) - } -} - -struct ServiceCommandExitStatus { - status: ExitStatus, - service_command: ServiceCommand, -} - -impl ServiceCommandExitStatus { - fn must_succeed(self) -> Result<(), SystemServiceError> { - if self.status.success() { - Ok(()) - } else { - Err(BsdServiceError::ServiceCommandFailed { - service_command: self.service_command.to_string(), - code: self.status.code(), - } - .into()) - } - } - - fn success(self) -> bool { - self.status.success() - } -} - -const SERVICE_BIN: &str = "/usr/sbin/service"; - -#[derive(Debug, Copy, Clone)] -enum ServiceCommand { - CheckManager, - Stop(SystemService), - Restart(SystemService), - Enable(SystemService), - Disable(SystemService), - IsActive(SystemService), -} - -impl ServiceCommand { - fn to_string(self) -> String { - match self { - Self::CheckManager => format!("{} -l", SERVICE_BIN), - Self::Stop(service) => format!( - "{} {} stop", - SERVICE_BIN, - SystemService::as_service_name(service) - ), - Self::Restart(service) => { - format!( - "{} {} restart", - SERVICE_BIN, - SystemService::as_service_name(service) - ) - } - Self::Enable(service) => { - format!( - "{} {} enable", - SERVICE_BIN, - SystemService::as_service_name(service) - ) - } - Self::Disable(service) => { - format!( - "{} {} forcedisable", - SERVICE_BIN, - SystemService::as_service_name(service) - ) - } - Self::IsActive(service) => { - format!( - "{} {} status", - SERVICE_BIN, - SystemService::as_service_name(service) - ) - } - } - } - - fn into_command(self) -> std::process::Command { - match self { - Self::CheckManager => CommandBuilder::new(SERVICE_BIN).arg("-l").silent().build(), - Self::Stop(service) => CommandBuilder::new(SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - .arg("stop") - .silent() - .build(), - Self::Restart(service) => CommandBuilder::new(SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - .arg("restart") - .silent() - .build(), - Self::Enable(service) => CommandBuilder::new(SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - .arg("enable") - .silent() - .build(), - - Self::Disable(service) => CommandBuilder::new(SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - // - // Use "forcedisable" as otherwise it could fail if you have a commented out - // `# mosquitto_enable="YES"` or - // `# mosquitto_enable="NO"` in your `/etc/rc.conf` file. - // - .arg("forcedisable") - .silent() - .build(), - Self::IsActive(service) => CommandBuilder::new(SERVICE_BIN) - .arg(SystemService::as_service_name(service)) - .arg("status") - .silent() - .build(), - } - } -} diff --git a/crates/core/tedge/src/system_services/managers/config.rs b/crates/core/tedge/src/system_services/managers/config.rs new file mode 100644 index 00000000..af495f02 --- /dev/null +++ b/crates/core/tedge/src/system_services/managers/config.rs @@ -0,0 +1,138 @@ +use crate::system_services::*; +use serde::Deserialize; +use std::fs; +use std::path::PathBuf; + +pub const SERVICE_CONFIG_FILE: &str = "system.toml"; + +#[derive(Deserialize, Debug, PartialEq)] +pub struct SystemConfig { + pub(crate) init: InitConfig, +} + +#[derive(Deserialize, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct InitConfig { + pub name: String, + pub is_available: Vec<String>, + pub restart: Vec<String>, + pub stop: Vec<String>, + pub enable: Vec<String>, + pub disable: Vec<String>, + pub is_active: Vec<String>, +} + +impl Default for InitConfig { + fn default() -> Self { + Self { + name: "systemd".to_string(), + is_available: vec!["/bin/systemctl".into(), "--version".into()], + restart: vec!["/bin/systemctl".into(), "restart".into(), "{}".into()], + stop: vec!["/bin/systemctl".into(), "stop".into(), "{}".into()], + enable: vec!["/bin/systemctl".into(), "enable".into(), "{}".into()], + disable: vec!["/bin/systemctl".into(), "disable".into(), "{}".into()], + is_active: vec!["/bin/systemctl".into(), "is-active".into(), "{}".into()], + } + } +} + +impl Default for SystemConfig { + fn default() -> Self { + Self { + init: InitConfig::default(), + } + } +} + +impl SystemConfig { + pub fn try_new(config_root: PathBuf) -> Result<Self, SystemServiceError> { + let config_path = config_root.join(SERVICE_CONFIG_FILE); + let config_path_str = config_path.to_str().unwrap_or(SERVICE_CONFIG_FILE); + + match fs::read_to_string(config_path.clone()) { + Ok(contents) => { + let config: SystemConfig = toml::from_str(contents.as_str()).map_err(|e| { + SystemServiceError::SystemConfigInvalidToml { + path: config_path_str.to_string(), + reason: format!("{}", e), + } + })?; + Ok(config) + } + Err(_) => { + println!("The system config file '{}' doesn't exist. Use '/bin/systemctl' as a service manager.\n", config_path_str); + Ok(Self::default()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::TempDir; + + #[test] + fn deserialize_system_config() { + let config: SystemConfig = toml::from_str( + r#" + [init] + name = "systemd" + is_available = ["/bin/systemctl", "--version"] + restart = ["/bin/systemctl", "restart", "{}"] + stop = ["/bin/systemctl", "stop", "{}"] + enable = ["/bin/systemctl", "enable", "{}"] + disable = ["/bin/systemctl", "disable", "{}"] + is_active = ["/bin/systemctl", "is-active", "{}"] + "#, + ) + .unwrap(); + + assert_eq!(config.init.name, "systemd"); + assert_eq!( + config.init.is_available, + vec!["/bin/systemctl", "--version"] + ); + assert_eq!(config.init.restart, vec!["/bin/systemctl", "restart", "{}"]); + assert_eq!(config.init.stop, vec!["/bin/systemctl", "stop", "{}"]); + assert_eq!(config.init.enable, vec!["/bin/systemctl", "enable", "{}"]); + assert_eq!(config.init.disable, vec!["/bin/systemctl", "disable", "{}"]); + assert_eq!( + config.init.is_active, + vec!["/bin/systemctl", "is-active", "{}"] + ); + } + + #[test] + fn read_system_config_file() -> anyhow::Result<()> { + let toml_conf = r#" + [init] + name = "systemd" + is_available = ["/bin/systemctl", "--version"] + restart = ["/bin/systemctl", "restart", "{}"] + stop = ["/bin/systemctl", "stop", "{}"] + enable = ["/bin/systemctl", "enable", "{}"] + disable = ["/bin/systemctl", "disable", "{}"] + is_active = ["/bin/systemctl", "is-active", "{}"] + "#; + let expected_config: SystemConfig = toml::from_str(toml_conf)?; + + let (_dir, config_root_path) = create_temp_system_config(toml_conf)?; + let config = SystemConfig::try_new(config_root_path).unwrap(); + + assert_eq!(config, expected_config); + + Ok(()) + } + + // Need to return TempDir, otherwise the dir will be deleted when this function ends. + fn create_temp_system_config(content: &str) -> std::io::Result<(TempDir, PathBuf)> { + let temp_dir = TempDir::new()?; + let config_root = temp_dir.path().to_path_buf(); + let config_file_path = config_root.join(SERVICE_CONFIG_FILE); + let mut file = std::fs::File::create(config_file_path.as_path())?; + file.write_all(content.as_bytes())?; + Ok((temp_dir, config_root)) + } +} diff --git a/crates/core/tedge/src/system_services/managers/general_manager.rs b/crates/core/tedge/src/system_services/managers/general_manager.rs new file mode 100644 index 00000000..fad556e3 --- /dev/null +++ b/crates/core/tedge/src/system_services/managers/general_manager.rs @@ -0,0 +1,401 @@ +use crate::system_services::{ + CommandBuilder, InitConfig, SystemConfig, SystemService, SystemServiceError, + SystemServiceManager, SERVICE_CONFIG_FILE, +}; +use std::fmt; +use std::path::PathBuf; +use std::process::ExitStatus; +use tedge_users::{UserManager, ROOT_USER}; + +#[derive(Debug)] +pub struct GeneralServiceManager { + user_manager: UserManager, + init_config: InitConfig, + config_path: String, +} + +impl GeneralServiceManager { + pub fn try_new( + user_manager: UserManager, + config_root: PathBuf, + ) -> Result<Self, SystemServiceError> { + let init_config = SystemConfig::try_new(config_root.clone())?.init; + let config_path = config_root + .join(SERVICE_CONFIG_FILE) + .to_str() + .unwrap_or(SERVICE_CONFIG_FILE) + .to_string(); + Ok(Self { + user_manager, + init_config, + config_path, + }) + } +} + +impl SystemServiceManager for GeneralServiceManager { + fn name(&self) -> &str { + &self.init_config.name + } + + fn check_operational(&self) -> Result<(), SystemServiceError> { + let exec_command = ServiceCommand::CheckManager.try_exec_command(self)?; + + match exec_command.to_command().status() { + Ok(status) if status.success() => Ok(()), + _ => Err(SystemServiceError::ServiceManagerUnavailable { + cmd: exec_command.to_string(), + name: self.name().to_string(), + }), + } + } + + fn stop_service(&self, service: SystemService) -> Result<(), SystemServiceError> { + let exec_command = ServiceCommand::Stop(service).try_exec_command(self)?; + self.run_service_command_as_root(exec_command, self.config_path.as_str())? + .must_succeed() + } + + fn restart_service(&self, service: SystemService) -> Result<(), SystemServiceError> { + let exec_command = ServiceCommand::Restart(service).try_exec_command(self)?; + self.run_service_command_as_root(exec_command, self.config_path.as_str())? + .must_succeed() + } + + fn enable_service(&self, service: SystemService) -> Result<(), SystemServiceError> { + let exec_command = ServiceCommand::Enable(service).try_exec_command(self)?; + self.run_service_command_as_root(exec_command, self.config_path.as_str())? + .must_succeed() + } + + fn disable_service(&self, service: SystemService) -> Result<(), SystemServiceError> { + let exec_command = ServiceCommand::Disable(service).try_exec_command(self)?; + self.run_service_command_as_root(exec_command, self.config_path.as_str())? + .must_succeed() + } + + fn is_service_running(&self, service: SystemService) -> Result<bool, SystemServiceError> { + let exec_command = ServiceCommand::IsActive(service).try_exec_command(self)?; + self.run_service_command_as_root(exec_command, self.config_path.as_str()) + .map(|status| status.success()) + } +} + +#[derive(Debug, PartialEq)] +struct ExecCommand { + exec: String, + args: Vec<String>, +} + +impl ExecCommand { + fn try_new( + config: Vec<String>, + cmd: ServiceCommand, + config_path: String, + ) -> Result<Self, SystemServiceError> { + match config.split_first() { + Some((exec, args)) => Ok(Self { + exec: exec.to_string(), + args: args.to_vec(), + }), + None => Err(SystemServiceError::SystemConfigInvalidSyntax { + reason: "Requires 1 or more arguments.".to_string(), + cmd: cmd.to_string(), + path: config_path, + }), + } + } + + fn try_new_with_placeholder( + config: Vec<String>, + service_cmd: ServiceCommand, + config_path: String, + service: SystemService, + ) -> Result<Self, SystemServiceError> { + let replaced = + replace_with_service_name(&config, service_cmd, config_path.as_str(), service)?; + Self::try_new(replaced, service_cmd, config_path) + } + + fn to_command(&self) -> std::process::Command { + CommandBuilder::new(&self.exec) + .args(&self.args) + .silent() + .build() + } +} + +impl fmt::Display for ExecCommand { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.args.is_empty() { + write!(f, "{}", self.exec) + } else { + let mut s = self.exec.to_owned(); + for arg in &self.args { + s = format!("{} {}", s, arg); + } + write!(f, "{}", s) + } + } +} + +fn replace_with_service_name( + input_args: &[String], + service_cmd: ServiceCommand, + config_path: &str, + service: SystemService, +) -> Result<Vec<String>, SystemServiceError> { + if !input_args.iter().any(|s| s == "{}") { + return Err(SystemServiceError::SystemConfigInvalidSyntax { + reason: "A placeholder '{}' is missing.".to_string(), + cmd: service_cmd.to_string(), + path: config_path.to_string(), + }); + } + + let mut args = input_args.to_owned(); + for item in args.iter_mut() { + if item == "{}" { + *item = SystemService::as_service_name(service).to_string(); + } + } + + Ok(args) +} + +#[derive(Debug, Copy, Clone)] +enum ServiceCommand { + CheckManager, + Stop(SystemService), + Restart(SystemService), + Enable(SystemService), + Disable(SystemService), + IsActive(SystemService), +} + +impl ServiceCommand { + fn try_exec_command( + &self, + service_manager: &GeneralServiceManager, + ) -> Result<ExecCommand, SystemServiceError> { + let config_path = service_manager.config_path.clone(); + match self { + Self::CheckManager => ExecCommand::try_new( + service_manager.init_config.is_available.clone(), + ServiceCommand::CheckManager, + config_path, + ), + Self::Stop(service) => ExecCommand::try_new_with_placeholder( + service_manager.init_config.stop.clone(), + ServiceCommand::Stop(*service), + config_path, + *service, + ), + Self::Restart(service) => ExecCommand::try_new_with_placeholder( + service_manager.init_config.restart.clone(), + ServiceCommand::Restart(*service), + config_path, + *service, + ), + Self::Enable(service) => ExecCommand::try_new_with_placeholder( + service_manager.init_config.enable.clone(), + ServiceCommand::Enable(*service), + config_path, + *service, + ), + Self::Disable(service) => ExecCommand::try_new_with_placeholder( + service_manager.init_config.disable.clone(), + ServiceCommand::Disable(*service), + config_path, + *service, + ), + Self::IsActive(service) => ExecCommand::try_new_with_placeholder( + service_manager.init_config.is_active.clone(), + ServiceCommand::IsActive(*service), + config_path, + *service, + ), + } + } +} + +impl fmt::Display for ServiceCommand { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::CheckManager => write!(f, "is_available"), + Self::Stop(_service) => write!(f, "stop"), + Self::Restart(_service) => write!(f, "restart"), + Self::Enable(_service) => write!(f, "enable"), + Self::Disable(_service) => write!(f, "disable"), + Self::IsActive(_service) => write!(f, "is_active"), + } + } +} + +impl GeneralServiceManager { + fn run_service_command_as_root( + &self, + exec_command: ExecCommand, + config_path: &str, + ) -> Result<ServiceCommandExitStatus, SystemServiceError> { + let _root_guard = self.user_manager.become_user(ROOT_USER); + + exec_command + .to_command() + .status() + .map_err(|_| SystemServiceError::ServiceCommandNotFound { + service_command: exec_command.to_string(), + path: config_path.to_string(), + }) + .map(|status| ServiceCommandExitStatus { + status, + service_command: exec_command.to_string(), + }) + } +} + +#[derive(Debug)] +struct ServiceCommandExitStatus { + status: ExitStatus, + service_command: String, +} + +impl ServiceCommandExitStatus { + fn must_succeed(self) -> Result<(), SystemServiceError> { + if self.status.success() { + Ok(()) + } else { + match self.status.code() { + Some(code) => Err(SystemServiceError::ServiceCommandFailedWithCode { + service_command: self.service_command, + code, + }), + None => Err(SystemServiceError::ServiceCommandFailedBySignal { + service_command: self.service_command, + }), + } + } + } + + fn success(self) -> bool { + self.status.success() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::*; + use test_case::test_case; + + #[test_case( + vec!["bin".to_string(), "{}".to_string(), "arg2".to_string()], + vec!["bin".to_string(), "mosquitto".to_string(), "arg2".to_string()] + ;"one placeholder")] + #[test_case( + vec!["bin".to_string(), "{}".to_string(), "{}".to_string()], + vec!["bin".to_string(), "mosquitto".to_string(), "mosquitto".to_string()] + ;"several placeholders")] + fn replace_placeholder_with_service(input: Vec<String>, expected_output: Vec<String>) { + let replaced_config = replace_with_service_name( + &input, + ServiceCommand::Stop(SystemService::Mosquitto), + "/dummy/path.toml", + SystemService::Mosquitto, + ) + .unwrap(); + assert_eq!(replaced_config, expected_output) + } + + #[test] + fn fail_to_replace_placeholder_with_service() { + let input = vec!["bin".to_string(), "arg1".to_string(), "arg2".to_string()]; + let system_config_error = replace_with_service_name( + &input, + ServiceCommand::Stop(SystemService::Mosquitto), + "dummy/path.toml", + SystemService::Mosquitto, + ) + .unwrap_err(); + assert_matches!( + system_config_error, + SystemServiceError::SystemConfigInvalidSyntax { .. } + ) + } + + #[test_case( + vec!["bin".to_string(), "arg1".to_string(), "arg2".to_string()], + ExecCommand { + exec: "bin".to_string(), + args: vec!["arg1".to_string(), "arg2".to_string()] + } + ;"with arguments")] + #[test_case( + vec!["bin".to_string()], + ExecCommand { + exec: "bin".to_string(), + args: vec![] + } + ;"only executable")] + fn build_exec_command(config: Vec<String>, expected: ExecCommand) { + let exec_command = ExecCommand::try_new( + config, + ServiceCommand::Stop(SystemService::Mosquitto), + "test/dummy.toml".to_string(), + ) + .unwrap(); + assert_eq!(exec_command, expected); + } + + #[test] + fn fail_to_build_exec_command() { + let config = vec![]; + let system_config_error = ExecCommand::try_new( + config, + ServiceCommand::Stop(SystemService::Mosquitto), + "test/dummy.toml".to_string(), + ) + .unwrap_err(); + assert_matches!( + system_config_error, + SystemServiceError::SystemConfigInvalidSyntax { .. } + ); + } + + #[test_case( + ExecCommand { + exec: "bin".to_string(), + args: vec!["arg1".to_string(), "arg2".to_string()] + }, + r#""bin" "arg1" "arg2""# + ;"with a |