use std::env;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use service_manager::{
RestartPolicy, ServiceInstallCtx, ServiceLabel, ServiceLevel, ServiceManager, ServiceStartCtx,
ServiceStatusCtx, ServiceStopCtx, ServiceUninstallCtx,
};
use thiserror::Error;
const SERVICE_LABEL: &str = "dev.rye.aranet";
#[derive(Debug, Error)]
pub enum ServiceError {
#[error("No service manager available on this platform")]
NoServiceManager,
#[error("Service manager error: {0}")]
Manager(#[from] std::io::Error),
#[error("Could not find aranet-service executable")]
ExecutableNotFound,
#[error("User-level services not supported on this platform")]
UserLevelNotSupported,
#[error("Failed to resolve current working directory: {0}")]
CurrentDirectory(#[source] std::io::Error),
}
#[derive(Debug, Clone, Copy, Default)]
pub enum Level {
#[default]
System,
User,
}
fn get_manager(level: Level) -> Result<Box<dyn ServiceManager>, ServiceError> {
let mut manager = <dyn ServiceManager>::native().map_err(|_| ServiceError::NoServiceManager)?;
let service_level = match level {
Level::System => ServiceLevel::System,
Level::User => ServiceLevel::User,
};
manager
.set_level(service_level)
.map_err(|_| ServiceError::UserLevelNotSupported)?;
Ok(manager)
}
fn get_executable_path() -> Result<PathBuf, ServiceError> {
let candidates = get_install_candidates();
for candidate in candidates {
if candidate.is_file() {
return Ok(candidate);
}
}
env::current_exe().map_err(|_| ServiceError::ExecutableNotFound)
}
fn get_install_candidates() -> Vec<PathBuf> {
let mut candidates = Vec::new();
#[cfg(target_os = "macos")]
{
candidates.push(PathBuf::from("/usr/local/bin/aranet-service"));
candidates.push(PathBuf::from("/opt/homebrew/bin/aranet-service"));
if let Some(home) = dirs::home_dir() {
candidates.push(home.join(".cargo/bin/aranet-service"));
candidates.push(home.join("Library/bin/aranet-service"));
}
}
#[cfg(target_os = "linux")]
{
candidates.push(PathBuf::from("/usr/local/bin/aranet-service"));
candidates.push(PathBuf::from("/usr/bin/aranet-service"));
if let Some(home) = dirs::home_dir() {
candidates.push(home.join(".cargo/bin/aranet-service"));
candidates.push(home.join(".local/bin/aranet-service"));
}
}
#[cfg(target_os = "windows")]
{
if let Ok(program_files) = env::var("ProgramFiles") {
candidates
.push(PathBuf::from(&program_files).join("aranet-service/aranet-service.exe"));
}
if let Some(home) = dirs::home_dir() {
candidates.push(home.join(".cargo/bin/aranet-service.exe"));
}
}
candidates
}
fn get_label() -> ServiceLabel {
SERVICE_LABEL.parse().expect("Invalid service label")
}
pub fn install(level: Level, options: &aranet_service::RunOptions) -> Result<(), ServiceError> {
let manager = get_manager(level)?;
let program = get_executable_path()?;
let label = get_label();
let args = build_install_args(options)?;
manager
.install(ServiceInstallCtx {
label,
program,
args,
contents: None,
username: None,
working_directory: None,
environment: None,
autostart: true,
restart_policy: RestartPolicy::OnFailure {
delay_secs: Some(5),
},
})
.map_err(ServiceError::Manager)?;
Ok(())
}
fn build_install_args(options: &aranet_service::RunOptions) -> Result<Vec<OsString>, ServiceError> {
let mut args = vec![OsString::from("run")];
if let Some(config) = &options.config {
args.push(OsString::from("--config"));
args.push(resolve_service_path(config)?.into_os_string());
}
if let Some(bind) = &options.bind {
args.push(OsString::from("--bind"));
args.push(OsString::from(bind));
}
if let Some(database) = &options.database {
args.push(OsString::from("--database"));
args.push(resolve_service_path(database)?.into_os_string());
}
if options.no_collector {
args.push(OsString::from("--no-collector"));
}
Ok(args)
}
fn resolve_service_path(path: &Path) -> Result<PathBuf, ServiceError> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
Ok(env::current_dir()
.map_err(ServiceError::CurrentDirectory)?
.join(path))
}
}
pub fn uninstall(level: Level) -> Result<(), ServiceError> {
let manager = get_manager(level)?;
let label = get_label();
manager
.uninstall(ServiceUninstallCtx { label })
.map_err(ServiceError::Manager)?;
Ok(())
}
pub fn start(level: Level) -> Result<(), ServiceError> {
let manager = get_manager(level)?;
let label = get_label();
manager
.start(ServiceStartCtx { label })
.map_err(ServiceError::Manager)?;
Ok(())
}
pub fn stop(level: Level) -> Result<(), ServiceError> {
let manager = get_manager(level)?;
let label = get_label();
manager
.stop(ServiceStopCtx { label })
.map_err(ServiceError::Manager)?;
Ok(())
}
pub fn status(level: Level) -> Result<ServiceStatus, ServiceError> {
let manager = get_manager(level)?;
let label = get_label();
match manager.status(ServiceStatusCtx { label }) {
Ok(status) => match status {
service_manager::ServiceStatus::Running => Ok(ServiceStatus::Running),
service_manager::ServiceStatus::Stopped(_) => Ok(ServiceStatus::Stopped),
service_manager::ServiceStatus::NotInstalled => Ok(ServiceStatus::Stopped),
},
Err(_) => {
if is_service_reachable() {
Ok(ServiceStatus::Running)
} else {
Ok(ServiceStatus::Stopped)
}
}
}
}
fn is_service_reachable() -> bool {
use std::net::{SocketAddr, TcpStream};
use std::time::Duration;
let bind = aranet_service::Config::load_default()
.map(|c| c.server.bind)
.unwrap_or_else(|_| "127.0.0.1:8080".to_string());
let addr: SocketAddr = match bind.parse() {
Ok(addr) => addr,
Err(_) => return false,
};
TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok()
}
#[derive(Debug, Clone, Copy)]
pub enum ServiceStatus {
Running,
Stopped,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_install_args_preserves_runtime_options() {
let db_path = env::temp_dir().join("aranet-test.db");
let options = aranet_service::RunOptions {
config: Some(PathBuf::from("config/server.toml")),
bind: Some("0.0.0.0:9090".to_string()),
database: Some(db_path.clone()),
no_collector: true,
};
let args = build_install_args(&options).unwrap();
let cwd = env::current_dir().unwrap();
assert_eq!(
args,
vec![
OsString::from("run"),
OsString::from("--config"),
cwd.join("config/server.toml").into_os_string(),
OsString::from("--bind"),
OsString::from("0.0.0.0:9090"),
OsString::from("--database"),
db_path.into_os_string(),
OsString::from("--no-collector"),
]
);
}
#[test]
fn test_build_install_args_omits_unset_options() {
let args = build_install_args(&aranet_service::RunOptions::default()).unwrap();
assert_eq!(args, vec![OsString::from("run")]);
}
}