use std::fmt;
pub mod windows;
#[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 },
AccessDenied,
Timeout { waited_ms: u64 },
}
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}")
}
Self::AccessDenied => write!(f, "access denied to service"),
Self::Timeout { waited_ms } => {
write!(f, "service state transition timed out after {waited_ms}ms")
}
}
}
}
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,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceState {
NotInstalled,
Stopped,
StartPending,
Running,
StopPending,
}
impl ServiceState {
#[must_use]
pub fn is_active(&self) -> bool {
matches!(self, Self::Running | Self::StartPending)
}
}
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>;
}
#[cfg(target_os = "windows")]
#[must_use]
pub fn platform_service_manager() -> Box<dyn ServiceManager> {
Box::new(windows::WindowsServiceManager::production())
}
#[cfg(test)]
mod tests {
use super::*;
#[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"));
}
}