use std::fmt;
pub mod launchd;
pub mod systemd;
#[derive(Debug)]
pub enum ServiceError {
CommandFailed {
command: String,
exit_status: Option<i32>,
stderr: String,
},
ExecFailed {
command: String,
source: std::io::Error,
},
NotAvailable { platform: String },
StateQueryFailed { source: std::io::Error },
}
impl fmt::Display for ServiceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CommandFailed {
command,
exit_status,
stderr,
} => {
write!(f, "command `{command}` failed")?;
if let Some(status) = exit_status {
write!(f, " (exit status: {status})")?;
}
if !stderr.is_empty() {
write!(f, ": {stderr}")?;
}
Ok(())
}
Self::ExecFailed { command, source } => {
write!(f, "failed to execute `{command}`: {source}")
}
Self::NotAvailable { platform } => {
write!(f, "service manager not available on {platform}")
}
Self::StateQueryFailed { source } => {
write!(f, "failed to query service state: {source}")
}
}
}
}
impl std::error::Error for ServiceError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::ExecFailed { source, .. } | Self::StateQueryFailed { source } => Some(source),
_ => None,
}
}
}
pub trait ServiceManager: Send + Sync {
fn start(&self) -> Result<(), ServiceError>;
fn stop(&self) -> Result<(), ServiceError>;
fn restart(&self) -> Result<(), ServiceError>;
fn is_active(&self) -> Result<bool, ServiceError>;
}
#[derive(Debug, Default)]
pub struct NoopServiceManager;
impl ServiceManager for NoopServiceManager {
fn start(&self) -> Result<(), ServiceError> {
Ok(())
}
fn stop(&self) -> Result<(), ServiceError> {
Ok(())
}
fn restart(&self) -> Result<(), ServiceError> {
Ok(())
}
fn is_active(&self) -> Result<bool, ServiceError> {
Ok(false)
}
}
#[must_use]
pub fn platform_service_manager() -> Box<dyn ServiceManager> {
#[cfg(target_os = "linux")]
{
Box::new(systemd::SystemdManager::new())
}
#[cfg(target_os = "macos")]
{
Box::new(launchd::LaunchdManager::new())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
Box::new(NoopServiceManager)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn noop_manager_operations_succeed() {
let manager = NoopServiceManager;
assert!(manager.start().is_ok());
assert!(manager.stop().is_ok());
assert!(manager.restart().is_ok());
assert!(!manager.is_active().unwrap());
}
#[test]
fn service_error_display() {
let err = ServiceError::CommandFailed {
command: "systemctl stop greggd".into(),
exit_status: Some(1),
stderr: "unit not found".into(),
};
let msg = format!("{err}");
assert!(msg.contains("systemctl stop greggd"));
assert!(msg.contains("exit status: 1"));
assert!(msg.contains("unit not found"));
}
#[test]
fn service_error_exec_failed() {
let err = ServiceError::ExecFailed {
command: "systemctl".into(),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
};
let msg = format!("{err}");
assert!(msg.contains("systemctl"));
assert!(msg.contains("not found"));
}
#[test]
fn service_error_not_available() {
let err = ServiceError::NotAvailable {
platform: "windows".into(),
};
let msg = format!("{err}");
assert!(msg.contains("windows"));
}
}