use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use uni_error::*;
use crate::manager::{
ServiceCapabilities, ServiceErrKind, ServiceManager, ServiceSpec, ServiceStatus,
};
use crate::unix_util::{SERVICE_PERMS, write_file};
const GLOBAL_PATH: &str = "/etc/systemd/system";
const SYSTEM_CTL: &str = "systemctl";
pub(crate) fn make_service_manager(
name: OsString,
_prefix: OsString,
user: bool,
) -> UniResult<Box<dyn ServiceManager>, ServiceErrKind> {
SystemDServiceManager::new(name, user).map(|mgr| Box::new(mgr) as Box<dyn ServiceManager>)
}
pub fn capabilities() -> ServiceCapabilities {
ServiceCapabilities::SUPPORTS_CUSTOM_GROUP | ServiceCapabilities::SUPPORTS_DESCRIPTION
}
struct SystemDServiceManager {
name: OsString,
user: bool,
}
impl SystemDServiceManager {
fn new(name: OsString, user: bool) -> UniResult<Self, ServiceErrKind> {
let mgr = Self { name, user };
if mgr.system_ctl(None).is_ok() {
Ok(mgr)
} else {
Err(UniError::from_kind_context(
ServiceErrKind::ServiceManagementNotAvailable,
"systemd is not available on this system",
))
}
}
fn system_ctl(&self, sub_cmd: Option<&str>) -> UniResult<(), ServiceErrKind> {
let mut command = Command::new(SYSTEM_CTL);
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if self.user {
command.arg("--user");
} else {
command.arg("--system");
}
if let Some(sub_cmd) = sub_cmd {
command.arg(sub_cmd).arg(&self.name);
}
let output = command.output().kind(ServiceErrKind::IoError)?;
if output.status.success() {
Ok(())
} else {
let msg = String::from_utf8(output.stderr).kind(ServiceErrKind::BadUtf8)?;
Err(ServiceErrKind::BadExitStatus(output.status.code(), msg).into_error())
}
}
fn path(&self) -> UniResult<PathBuf, ServiceErrKind> {
if self.user {
Ok(dirs::config_dir()
.ok_or_else(|| ServiceErrKind::DirectoryNotFound.into_error())?
.join("systemd")
.join("user"))
} else {
Ok(PathBuf::from(GLOBAL_PATH))
}
}
fn make_file_name(&self) -> UniResult<PathBuf, ServiceErrKind> {
let mut filename = OsString::new();
filename.push(&self.name);
filename.push(".service");
Ok(self.path()?.join(filename))
}
}
impl ServiceManager for SystemDServiceManager {
fn fully_qualified_name(&self) -> Cow<'_, OsStr> {
Cow::Borrowed(&self.name)
}
fn is_user_service(&self) -> bool {
self.user
}
fn install(&self, spec: &ServiceSpec) -> UniResult<(), ServiceErrKind> {
let wanted_by = if self.user {
"default.target"
} else {
"multi-user.target"
};
let args = spec.path_and_args_string()?.join(" ");
let desc = match spec.description_string()? {
Some(desc) => format!("Description={desc}\n"),
None => String::new(),
};
let restart = if spec.restart_on_failure {
"on-failure"
} else {
"no"
};
let user = match spec.user_string()? {
Some(user) => format!("User={user}\n"),
None => String::new(),
};
let group = match spec.group_string()? {
Some(group) => format!("Group={group}\n"),
None => String::new(),
};
let service = format!(
r#"[Unit]
{desc}
[Service]
ExecStart={args}
Restart={restart}
RestartSec=2
{user}{group}
[Install]
WantedBy={wanted_by}
"#
);
let path = self.path()?;
fs::create_dir_all(&path).kind(ServiceErrKind::IoError)?;
let file = self.make_file_name()?;
write_file(&file, &service, SERVICE_PERMS)?;
if spec.autostart {
self.system_ctl(Some("enable"))?;
}
Ok(())
}
fn uninstall(&self) -> UniResult<(), ServiceErrKind> {
self.system_ctl(Some("disable"))?;
let file = self.make_file_name()?;
fs::remove_file(file).kind(ServiceErrKind::IoError)?;
Ok(())
}
fn start(&self) -> UniResult<(), ServiceErrKind> {
self.system_ctl(Some("start"))
}
fn stop(&self) -> UniResult<(), ServiceErrKind> {
self.system_ctl(Some("stop"))
}
fn status(&self) -> UniResult<ServiceStatus, ServiceErrKind> {
match self.system_ctl(Some("status")) {
Ok(_) => Ok(ServiceStatus::Running),
Err(e) if matches!(e.kind_ref(), ServiceErrKind::BadExitStatus(_, _)) => {
match e.kind_ref() {
ServiceErrKind::BadExitStatus(Some(3), _) => Ok(ServiceStatus::Stopped),
ServiceErrKind::BadExitStatus(Some(4), _) => Ok(ServiceStatus::NotInstalled),
_ => Err(e),
}
}
Err(err) => Err(err),
}
}
}