diff options
author | Michael Neumann <mneumann@ntecs.de> | 2021-07-13 10:05:47 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-13 10:05:47 +0200 |
commit | 33ace1c0cc88cf54bced5e04a229c690a3ffe08b (patch) | |
tree | 4d40823ed4a957d5dcec7b156ab6bbaf367e6fd9 /tedge | |
parent | e9a75e9e2198ea54327cb5cb9363159efede6933 (diff) |
Add system service manager abstraction (#338)
* Refactor code to use abstract `SystemServiceManager`.
* Implement Debug for UserManager
This simplifies code that embeds a `UserManager` and has to implement
`Debug` on it's own as `Debug` cannot be automatically derived.
* Add `SystemdServiceManager`
* Add NullSystemServiceManager that always fail
This can be used as fallback manager where commands that don't need a
SystemServiceManager would still work.
* Bring in support for OpenRC / BSD service(8)
* Add feature flag "openrc" to override the default system service
manager that is selected based on the target operation system.
For instance, Gentoo has target_os = "linux", but uses OpenRC.
OpenRC can also be used on other systems.
* Code roughly based on PR #196.
Diffstat (limited to 'tedge')
24 files changed, 951 insertions, 393 deletions
diff --git a/tedge/Cargo.toml b/tedge/Cargo.toml index 412c14f4..2326275a 100644 --- a/tedge/Cargo.toml +++ b/tedge/Cargo.toml @@ -46,3 +46,4 @@ integration-test = [] mosquitto-available = [] # Enable tests requesting mosquitto installed root-access = [] # Enable tests requesting root access tedge-user = [] # Enable tests requesting a tedge user +openrc = [] # Enable usage of OpenRC diff --git a/tedge/src/cli/connect/cli.rs b/tedge/src/cli/connect/cli.rs index ed2539ae..42e68885 100644 --- a/tedge/src/cli/connect/cli.rs +++ b/tedge/src/cli/connect/cli.rs @@ -32,6 +32,7 @@ impl BuildCommand for TEdgeConnectOpt { cloud: Cloud::C8y, common_mosquitto_config: CommonMosquittoConfig::default(), is_test_connection, + service_manager: context.service_manager.clone(), }, TEdgeConnectOpt::Az { is_test_connection } => ConnectCommand { config_location: context.config_location, @@ -39,6 +40,7 @@ impl BuildCommand for TEdgeConnectOpt { cloud: Cloud::Azure, common_mosquitto_config: CommonMosquittoConfig::default(), is_test_connection, + service_manager: context.service_manager.clone(), }, } .into_boxed()) diff --git a/tedge/src/cli/connect/command.rs b/tedge/src/cli/connect/command.rs index ca4dcc3d..a4452380 100644 --- a/tedge/src/cli/connect/command.rs +++ b/tedge/src/cli/connect/command.rs @@ -1,18 +1,15 @@ use crate::{ cli::connect::*, command::{Command, ExecutionContext}, - services::{ - self, mosquitto::MosquittoService, tedge_mapper_az::TedgeMapperAzService, - tedge_mapper_c8y::TedgeMapperC8yService, SystemdService, - }, + system_services::*, utils::paths, ConfigError, }; use mqtt_client::{Client, Message, MqttClient, Topic, TopicFilter}; use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::time::Duration; use tedge_config::*; -use tedge_users::{UserManager, ROOT_USER}; use tempfile::NamedTempFile; use tokio::time::timeout; use which::which; @@ -31,6 +28,7 @@ pub struct ConnectCommand { pub cloud: Cloud, pub common_mosquitto_config: CommonMosquittoConfig, pub is_test_connection: bool, + pub service_manager: Arc<dyn SystemServiceManager>, } pub enum Cloud { @@ -39,6 +37,15 @@ pub enum Cloud { } impl Cloud { + fn dependent_mapper_service(&self) -> SystemService { + match self { + Cloud::Azure => SystemService::TEdgeMapperAz, + Cloud::C8y => SystemService::TEdgeMapperC8y, + } + } +} + +impl Cloud { fn as_str(&self) -> &'static str { match self { Self::Azure => "Azure", @@ -56,7 +63,7 @@ impl Command for ConnectCommand { } } - fn execute(&self, context: &ExecutionContext) -> Result<(), anyhow::Error> { + fn execute(&self, _context: &ExecutionContext) -> Result<(), anyhow::Error> { let mut config = self.config_repository.load()?; if self.is_test_connection { @@ -82,7 +89,7 @@ impl Command for ConnectCommand { &bridge_config, &self.cloud, &updated_mosquitto_config, - &context.user_manager, + self.service_manager.as_ref(), &self.config_location, )?; @@ -282,11 +289,11 @@ fn new_bridge( bridge_config: &BridgeConfig, cloud: &Cloud, common_mosquitto_config: &CommonMosquittoConfig, - user_manager: &UserManager, + service_manager: &dyn SystemServiceManager, config_location: &TEdgeConfigLocation, ) -> Result<(), ConnectError> { - println!("Checking if systemd is available.\n"); - let () = services::systemd_available()?; + println!("Checking if {} is available.\n", service_manager.name()); + let () = service_manager.check_operational()?; println!("Checking if configuration for requested bridge already exists.\n"); let () = bridge_config_exists(config_location, bridge_config)?; @@ -304,7 +311,7 @@ fn new_bridge( } println!("Restarting mosquitto service.\n"); - if let Err(err) = MosquittoService.restart(user_manager) { + if let Err(err) = service_manager.restart_service(SystemService::Mosquitto) { clean_up(config_location, bridge_config)?; return Err(err.into()); } @@ -318,7 +325,7 @@ fn new_bridge( )); println!("Persisting mosquitto on reboot.\n"); - if let Err(err) = MosquittoService.enable(user_manager) { + if let Err(err) = service_manager.enable_service(SystemService::Mosquitto) { clean_up(config_location, bridge_config)?; return Err(err.into()); } @@ -331,14 +338,8 @@ fn new_bridge( if which("tedge_mapper").is_err() { println!("Warning: tedge_mapper is not installed. We recommend to install it.\n"); } else { - match cloud { - Cloud::Azure => { - start_and_enable_tedge_mapper_az(user_manager); - } - Cloud::C8y => { - start_and_enable_tedge_mapper_c8y(user_manager); - } - } + service_manager + .start_and_enable_service(cloud.dependent_mapper_service(), std::io::stdout()); } } @@ -395,48 +396,6 @@ fn write_bridge_config_to_file( Ok(()) } -fn start_and_enable_tedge_mapper_c8y(user_manager: &UserManager) { - let _root_guard = user_manager.become_user(ROOT_USER); - let mut failed = false; - - println!("Starting tedge-mapper service.\n"); - if let Err(err) = TedgeMapperC8yService.restart(user_manager) { - println!("Failed to stop tedge-mapper service: {:?}", err); - failed = true; - } - - println!("Persisting tedge-mapper on reboot.\n"); - if let Err(err) = TedgeMapperC8yService.enable(user_manager) { - println!("Failed to enable tedge-mapper service: {:?}", err); - failed = true; - } - - if !failed { - println!("tedge-mapper service successfully started and enabled!\n"); - } -} - -fn start_and_enable_tedge_mapper_az(user_manager: &UserManager) { - let _root_guard = user_manager.become_user(ROOT_USER); - let mut failed = false; - - println!("Starting tedge-mapper service.\n"); - if let Err(err) = TedgeMapperAzService.restart(user_manager) { - println!("Failed to stop tedge-mapper service: {:?}", err); - failed = true; - } - - println!("Persisting tedge-mapper on reboot.\n"); - if let Err(err) = TedgeMapperAzService.enable(user_manager) { - println!("Failed to enable tedge-mapper service: {:?}", err); - failed = true; - } - - if !failed { - println!("tedge-mapper service successfully started and enabled!\n"); - } -} - fn get_bridge_config_file_path( config_location: &TEdgeConfigLocation, bridge_config: &BridgeConfig, diff --git a/tedge/src/cli/connect/error.rs b/tedge/src/cli/connect/error.rs index c64553b4..426c628c 100644 --- a/tedge/src/cli/connect/error.rs +++ b/tedge/src/cli/connect/error.rs @@ -25,7 +25,7 @@ pub enum ConnectError { UrlParse(#[from] url::ParseError), #[error(transparent)] - ServicesError(#[from] crate::services::ServicesError), + SystemServiceError(#[from] crate::system_services::SystemServiceError), #[error("Operation timed out. Is mosquitto running?")] TimeoutElapsedError, diff --git a/tedge/src/cli/disconnect/cli.rs b/tedge/src/cli/disconnect/cli.rs index ad8721fa..d58508e4 100644 --- a/tedge/src/cli/disconnect/cli.rs +++ b/tedge/src/cli/disconnect/cli.rs @@ -21,12 +21,14 @@ impl BuildCommand for TEdgeDisconnectBridgeCli { config_file: C8Y_CONFIG_FILENAME.into(), cloud: Cloud::C8y, use_mapper: true, + service_manager: context.service_manager.clone(), }, TEdgeDisconnectBridgeCli::Az => DisconnectBridgeCommand { config_location: context.config_location, config_file: AZURE_CONFIG_FILENAME.into(), cloud: Cloud::Azure, use_mapper: true, + service_manager: context.service_manager.clone(), }, }; Ok(cmd.into_boxed()) diff --git a/tedge/src/cli/disconnect/disconnect_bridge.rs b/tedge/src/cli/disconnect/disconnect_bridge.rs index 1730682b..4f004323 100644 --- a/tedge/src/cli/disconnect/disconnect_bridge.rs +++ b/tedge/src/cli/disconnect/disconnect_bridge.rs @@ -1,21 +1,27 @@ use crate::cli::disconnect::error::*; use crate::command::*; -use crate::services::{ - mosquitto::MosquittoService, tedge_mapper_az::TedgeMapperAzService, - tedge_mapper_c8y::TedgeMapperC8yService, SystemdService, -}; +use crate::system_services::*; +use std::sync::Arc; use tedge_config::TEdgeConfigLocation; -use tedge_users::*; use which::which; const TEDGE_BRIDGE_CONF_DIR_PATH: &str = "mosquitto-conf"; -#[derive(Debug)] +#[derive(Copy, Clone, Debug)] pub enum Cloud { C8y, Azure, } +impl Cloud { + fn dependent_mapper_service(&self) -> SystemService { + match self { + Cloud::Azure => SystemService::TEdgeMapperAz, + Cloud::C8y => SystemService::TEdgeMapperC8y, + } + } +} + impl From<Cloud> for String { fn from(val: Cloud) -> Self { match val { @@ -31,6 +37,7 @@ pub struct DisconnectBridgeCommand { pub config_file: String, pub cloud: Cloud, pub use_mapper: bool, + pub service_manager: Arc<dyn SystemServiceManager>, } impl Command for DisconnectBridgeCommand { @@ -38,8 +45,8 @@ impl Command for DisconnectBridgeCommand { format!("remove the bridge to disconnect {:?} cloud", self.cloud) } - fn execute(&self, context: &ExecutionContext) -> Result<(), anyhow::Error> { - match self.stop_bridge(&context.user_manager) { + fn execute(&self, _context: &ExecutionContext) -> Result<(), anyhow::Error> { + match self.stop_bridge() { Ok(()) | Err(DisconnectBridgeError::BridgeFileDoesNotExist) => Ok(()), Err(err) => Err(err.into()), } @@ -47,23 +54,21 @@ impl Command for DisconnectBridgeCommand { } impl DisconnectBridgeCommand { - fn stop_bridge(&self, user_manager: &UserManager) -> Result<(), DisconnectBridgeError> { + fn service_manager(&self) -> &dyn SystemServiceManager { + self.service_manager.as_ref() + } + + fn stop_bridge(&self) -> Result<(), DisconnectBridgeError> { // If this fails, do not continue with applying changes and stopping/disabling tedge-mapper. self.remove_bridge_config_file()?; // Ignore failure - let _ = self.apply_changes_to_mosquitto(user_manager); + let _ = self.apply_changes_to_mosquitto(); // Only C8Y changes the status of tedge-mapper if self.use_mapper && which("tedge_mapper").is_ok() { - match self.cloud { - Cloud::Azure => { - self.stop_and_disable_tedge_mapper_az(user_manager); - } - Cloud::C8y => { - self.stop_and_disable_tedge_mapper_c8y(user_manager); - } - } + self.service_manager() + .stop_and_disable_service(self.cloud.dependent_mapper_service(), std::io::stdout()); } Ok(()) @@ -99,57 +104,15 @@ impl DisconnectBridgeCommand { // Deviation from specification: // Check if mosquitto is running, restart only if it was active before, if not don't do anything. - fn apply_changes_to_mosquitto( - &self, - user_manager: &UserManager, - ) -> Result<(), DisconnectBridgeError> { + fn apply_changes_to_mosquitto(&self) -> Result<(), DisconnectBridgeError> { println!("Applying changes to mosquitto.\n"); - if MosquittoService.is_active()? { - MosquittoService.restart(user_manager)?; + + if self + .service_manager() + .restart_service_if_running(SystemService::Mosquitto)? + { println!("{:?} Bridge successfully disconnected!\n", self.cloud); } Ok(()) } - - fn stop_and_disable_tedge_mapper_c8y(&self, user_manager: &UserManager) { - let _root_guard = user_manager.become_user(ROOT_USER); - let mut failed = false; - - println!("Stopping tedge-mapper service.\n"); - if let Err(err) = TedgeMapperC8yService.stop(user_manager) { - println!("Failed to stop tedge-mapper service: {:?}", err); - failed = true; - } - - println!("Disabling tedge-mapper service.\n"); - if let Err(err) = TedgeMapperC8yService.disable(user_manager) { - println!("Failed to disable tedge-mapper service: {:?}", err); - failed = true; - } - - if !failed { - println!("tedge-mapper service successfully stopped and disabled!\n"); - } - } - - fn stop_and_disable_tedge_mapper_az(&self, user_manager: &UserManager) { - let _root_guard = user_manager.become_user(ROOT_USER); - let mut failed = false; - - println!("Stopping tedge-mapper service.\n"); - if let Err(err) = TedgeMapperAzService.stop(user_manager) { - println!("Failed to stop tedge-mapper service: {:?}", err); - failed = true; - } - - println!("Disabling tedge-mapper service.\n"); - if let Err(err) = TedgeMapperAzService.disable(user_manager) { - println!("Failed to disable tedge-mapper service: {:?}", err); - failed = true; - } - - if !failed { - println!("tedge-mapper service successfully stopped and disabled!\n"); - } - } } diff --git a/tedge/src/cli/disconnect/error.rs b/tedge/src/cli/disconnect/error.rs index b24109bc..fd38bf0d 100644 --- a/tedge/src/cli/disconnect/error.rs +++ b/tedge/src/cli/disconnect/error.rs @@ -1,4 +1,3 @@ -use crate::services; use std::path::PathBuf; #[derive(thiserror::Error, Debug)] @@ -13,7 +12,7 @@ pub enum DisconnectBridgeError { IoError(#[from] std::io::Error), #[error(transparent)] - ServicesError(#[from] services::ServicesError), + SystemServiceError(#[from] crate::system_services::SystemServiceError), #[error("Bridge file does not exist.")] BridgeFileDoesNotExist, diff --git a/tedge/src/command.rs b/tedge/src/command.rs index 1c9cb3f2..3879c0b8 100644 --- a/tedge/src/command.rs +++ b/tedge/src/command.rs @@ -1,3 +1,5 @@ +use crate::system_services::*; +use std::sync::Arc; use tedge_users::UserManager; /// A trait to be implemented by all tedge sub-commands. @@ -162,6 +164,7 @@ pub trait BuildCommand { pub struct BuildContext { pub config_repository: tedge_config::TEdgeConfigRepository, pub config_location: tedge_config::TEdgeConfigLocation, + pub service_manager: Arc<dyn SystemServiceManager>, } /// The execution context of a command. diff --git a/tedge/src/main.rs b/tedge/src/main.rs index 2e429a86..bb467f84 100644 --- a/tedge/src/main.rs +++ b/tedge/src/main.rs @@ -1,13 +1,16 @@ #![forbid(unsafe_code)] #![deny(clippy::mem_forget)] +use crate::system_services::*; use anyhow::Context; +use std::sync::Arc; use structopt::StructOpt; +use tedge_users::UserManager; mod cli; mod command; mod error; -mod services; +mod system_services; mod utils; type ConfigError = crate::error::TEdgeError; @@ -34,6 +37,7 @@ fn main() -> anyhow::Result<()> { let build_context = BuildContext { config_repository, config_location: tedge_config_location, + service_manager: service_manager(context.user_manager.clone()), }; let cmd = opt @@ -44,3 +48,15 @@ fn main() -> anyhow::Result<()> { cmd.execute(&context) .with_context(|| format!("failed to {}", cmd.description())) } + +fn service_manager(user_manager: UserManager) -> Arc<dyn SystemServiceManager> { + if cfg!(feature = "openrc") { + Arc::new(OpenRcServiceManager::new(user_manager)) + } else if cfg!(target_os = "linux") { + Arc::new(SystemdServiceManager::new(user_manager)) + } else if cfg!(target_os = "freebsd") { + Arc::new(BsdServiceManager::new(user_manager)) + } else { + Arc::new(NullSystemServiceManager) + } +} diff --git a/tedge/src/services/mod.rs b/tedge/src/services/mod.rs deleted file mode 100644 index 3abed534..00000000 --- a/tedge/src/services/mod.rs +++ /dev/null @@ -1,240 +0,0 @@ -use crate::services::SystemdError::{ - ServiceNotFound, ServiceNotLoaded, SystemdNotAvailable, UnhandledReturnCode, UnspecificError, -}; -use crate::utils::paths; -use std::process::ExitStatus; -use tedge_users::*; - -pub mod mosquitto; -pub mod tedge_mapper_az; -pub mod tedge_mapper_c8y; - -type ExitCode = i32; - -const SYSTEMCTL_OK: ExitCode = 0; -const SYSTEMCTL_ERROR_GENERIC: ExitCode = 1; -const SYSTEMCTL_ERROR_UNIT_IS_NOT_ACTIVE: ExitCode = 3; -const SYSTEMCTL_ERROR_SERVICE_NOT_FOUND: ExitCode = 5; -const SYSTEMCTL_ERROR_SERVICE_NOT_LOADED: ExitCode = 5; - -const SYSTEMCTL_BIN: &str = "systemctl"; - -pub trait SystemdService { - const SERVICE_NAME: &'static str; - - fn stop(&self, user_manager: &UserManager) -> Result<(), ServicesError> { - match call_systemd_subcmd_sudo(SystemCtlCmd::Stop, Self::SERVICE_NAME, user_manager)? { - SYSTEMCTL_OK => Ok(()), - SYSTEMCTL_ERROR_GENERIC => Err(ServicesError::SystemdError(UnspecificError { - service: Self::SERVICE_NAME, - cmd: SystemCtlCmd::Stop.as_str(), - hint: "Lacking permissions.", - })), - SYSTEMCTL_ERROR_SERVICE_NOT_LOADED => { - Err(ServicesError::SystemdError(ServiceNotLoaded { - service: Self::SERVICE_NAME, - })) - } - code => Err(ServicesError::SystemdError(UnhandledReturnCode { code })), - } - } - - // Note that restarting a unit with this command does not necessarily flush out all of the unit's resources before it is started again. - // For example, the per-service file descriptor storage facility (see FileDescriptorStoreMax= in systemd.service(5)) will remain intact - // as long as the unit has a job pending, and is only cleared when the unit is fully stopped and no jobs are pending anymore. - // If it is intended that the file descriptor store is flushed out, too, during a restart operation an explicit - // systemctl stop command followed by systemctl start should be issued. - fn restart(&self, user_manager: &UserManager) -> Result<(), ServicesError> { - match call_systemd_subcmd_sudo(SystemCtlCmd::Restart, Self::SERVICE_NAME, user_manager)? { - SYSTEMCTL_OK => Ok(()), - SYSTEMCTL_ERROR_GENERIC => Err(ServicesError::SystemdError(UnspecificError { - service: Self::SERVICE_NAME, - cmd: SystemCtlCmd::Restart.as_str(), - hint: "Lacking permissions or service's process exited with error code.", - })), - SYSTEMCTL_ERROR_SERVICE_NOT_FOUND => { - Err(ServicesError::SystemdError(ServiceNotFound { - service: Self::SERVICE_NAME, - })) - } - code => Err(ServicesError::SystemdError(UnhandledReturnCode { code })), - } - } - - fn enable(&self, user_manager: &UserManager) -> Result<(), ServicesError> { - match call_systemd_subcmd_sudo(SystemCtlCmd::Enable, Self::SERVICE_NAME, user_manager)? { - SYSTEMCTL_OK => Ok(()), - SYSTEMCTL_ERROR_GENERIC => Err(ServicesError::SystemdError(UnspecificError { - service: Self::SERVICE_NAME, - cmd: SystemCtlCmd::Enable.as_str(), - hint: "Lacking permissions.", - })), - code => Err(ServicesError::SystemdError(UnhandledReturnCode { code })), - } - } - - fn disable(&self, user_manager: &UserManager) -> Result<(), ServicesError> { - match call_systemd_subcmd_sudo(SystemCtlCmd::Disable, Self::SERVICE_NAME, user_manager)? { - SYSTEMCTL_OK => Ok(()), - SYSTEMCTL_ERROR_GENERIC => Err(ServicesError::SystemdError(UnspecificError { - service: Self::SERVICE_NAME, - cmd: SystemCtlCmd::Disable.as_str(), - hint: "Lacking permissions.", - })), - code => Err(ServicesError::SystemdError(UnhandledReturnCode { code })), - } - } - - fn is_active(&self) -> Result<bool, ServicesError> { - match call_systemd_subcmd(SystemCtlCmd::IsActive, Self::SERVICE_NAME)? { - SYSTEMCTL_OK => Ok(true), - SYSTEMCTL_ERROR_UNIT_IS_NOT_ACTIVE => Ok(false), - code => Err(ServicesError::SystemdError(UnhandledReturnCode { code })), - } - } -} - -#[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 {service} not loaded.")] - ServiceNotLoaded { service: &'static str }, - - #[error( - "Systemd is not available on the system or elevated permissions have not been granted." - )] - SystemdNotAvailable, - - #[error("Returned exit code: '{code:?}' for: systemd' is unhandled.")] - UnhandledReturnCode { code: i32 }, -} - -#[derive(thiserror::Error, Debug)] -pub enum ServicesError { - #[error(transparent)] - IoError(#[from] std::io::Error), - - #[error(transparent)] - SystemdError(#[from] SystemdError), - - #[error(transparent)] - PathsError(#[from] paths::PathsError), - - #[error("Unexpected value for exit status.")] - UnexpectedExitStatus, -} - -fn cmd_nullstdio_args_with_code(command: &str, args: &[&str]) -> Result<ExitStatus, ServicesError> { - Ok(std::process::Command::new(command) - .args(args) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status()?) -} - -fn call_systemd_subcmd_sudo( - systemctl_subcmd: SystemCtlCmd, - arg: &str, - user_manager: &UserManager, -) -> Result<i32, ServicesError> { - let _root_guard = user_manager.become_user(ROOT_USER); - call_systemd_subcmd(systemctl_subcmd, arg) -} - -fn call_systemd_subcmd(systemctl_subcmd: SystemCtlCmd, arg: &str) -> Result<i32, ServicesError> { - cmd_nullstdio_args_with_code(SYSTEMCTL_BIN, &[systemctl_subcmd.as_str(), arg])? - .code() - .ok_or(ServicesError::UnexpectedExitStatus) -} - -pub(crate) fn systemd_available() -> Result<(), ServicesError> { - std::process::Command::new(SYSTEMCTL_BIN) - .arg(SystemCtlParam::Version.as_str()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - .map_or_else( - |_error| Err(ServicesError::SystemdError(SystemdNotAvailable)), - |status| { - if status.success() { - Ok(()) - } else { - Err(ServicesError::SystemdError(SystemdNotAvailable)) - } - }, - ) -} - -#[derive(Debug)] -enum SystemCtlCmd { - Enable, - Disable, - IsActive, - Stop, - Restart, -} - -impl SystemCtlCmd { - fn as_str(&self) -> &'static str { - match self { - SystemCtlCmd::Enable => "enable", - SystemCtlCmd::Disable => "disable", - SystemCtlCmd::IsActive => "is-active", - SystemCtlCmd::Stop => "stop", - SystemCtlCmd::Restart => "restart", - } - } -} - -impl From<SystemCtlCmd> for String { - fn from(val: SystemCtlCmd) -> Self { - val.as_str().into() - } -} - -#[derive(Debug)] -enum SystemCtlParam { - Version, -} - -impl SystemCtlParam { - fn as_str(&self) -> &'static str { - match self { - SystemCtlParam::Version => "--version", - } - } -} - -impl From<SystemCtlParam> for String { - fn from(val: SystemCtlParam) -> Self { - val.as_str().into() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn cmd_nullstdio_args_expected_exit_code_zero() { - // There is a chance that this may fail on very embedded system which will no |