use anyhow::{Context, Result};
use service_manager::*;
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;
const SERVICE_NAME: &str = "torc-server";
#[derive(Debug, Clone)]
pub enum ServiceCommand {
Install,
Uninstall,
Start,
Stop,
Status,
}
#[derive(Debug, Clone)]
pub struct ServiceConfig {
pub log_dir: Option<PathBuf>,
pub database: Option<String>,
pub host: String,
pub port: u16,
pub threads: u32,
pub auth_file: Option<String>,
pub require_auth: bool,
pub credential_cache_ttl_secs: u64,
pub enforce_access_control: bool,
pub log_level: String,
pub json_logs: bool,
pub https: bool,
pub tls_cert: Option<String>,
pub tls_key: Option<String>,
pub admin_users: Vec<String>,
pub completion_check_interval_secs: Option<f64>,
}
impl ServiceConfig {
pub const DEFAULT_SERVICE_INTERVAL_SECS: f64 = 5.0;
pub const DEFAULT_CREDENTIAL_CACHE_TTL_SECS: u64 = 60;
fn default_system() -> Self {
Self {
log_dir: Some(PathBuf::from("/var/log/torc")),
database: Some("/var/lib/torc/torc.db".to_string()),
host: "0.0.0.0".to_string(),
port: 8080,
threads: 4,
auth_file: None,
require_auth: false,
credential_cache_ttl_secs: Self::DEFAULT_CREDENTIAL_CACHE_TTL_SECS,
enforce_access_control: false,
log_level: "info".to_string(),
json_logs: false,
https: false,
tls_cert: None,
tls_key: None,
admin_users: Vec::new(),
completion_check_interval_secs: None, }
}
fn default_user() -> Self {
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
Self {
log_dir: Some(PathBuf::from(format!("{}/.torc/logs", home))),
database: Some(format!("{}/.torc/torc.db", home)),
host: "0.0.0.0".to_string(),
port: 8080,
threads: 4,
auth_file: None,
require_auth: false,
credential_cache_ttl_secs: Self::DEFAULT_CREDENTIAL_CACHE_TTL_SECS,
enforce_access_control: false,
log_level: "info".to_string(),
json_logs: false,
https: false,
tls_cert: None,
tls_key: None,
admin_users: Vec::new(),
completion_check_interval_secs: None, }
}
fn merge_with_defaults(user_config: &ServiceConfig, user_level: bool) -> Self {
let defaults = if user_level {
Self::default_user()
} else {
Self::default_system()
};
Self {
log_dir: user_config.log_dir.clone().or(defaults.log_dir),
database: user_config.database.clone().or(defaults.database),
auth_file: user_config.auth_file.clone().or(defaults.auth_file),
tls_cert: user_config.tls_cert.clone().or(defaults.tls_cert),
tls_key: user_config.tls_key.clone().or(defaults.tls_key),
completion_check_interval_secs: user_config.completion_check_interval_secs,
host: if user_config.host != "0.0.0.0" {
user_config.host.clone()
} else {
defaults.host
},
port: if user_config.port != 8080 {
user_config.port
} else {
defaults.port
},
threads: if user_config.threads != 1 {
user_config.threads
} else {
defaults.threads
},
log_level: if user_config.log_level != "info" {
user_config.log_level.clone()
} else {
defaults.log_level
},
credential_cache_ttl_secs: if user_config.credential_cache_ttl_secs
!= ServiceConfig::DEFAULT_CREDENTIAL_CACHE_TTL_SECS
{
user_config.credential_cache_ttl_secs
} else {
defaults.credential_cache_ttl_secs
},
require_auth: user_config.require_auth || defaults.require_auth,
enforce_access_control: user_config.enforce_access_control
|| defaults.enforce_access_control,
json_logs: user_config.json_logs || defaults.json_logs,
https: user_config.https || defaults.https,
admin_users: if user_config.admin_users.is_empty() {
defaults.admin_users
} else {
user_config.admin_users.clone()
},
}
}
}
impl Default for ServiceConfig {
fn default() -> Self {
Self::default_system()
}
}
fn get_service_manager(user_level: bool) -> Result<Box<dyn ServiceManager>> {
let level = if user_level {
ServiceLevel::User
} else {
ServiceLevel::System
};
let mut manager = <dyn ServiceManager>::native()
.context("Failed to detect native service manager for this platform")?;
manager
.set_level(level)
.context("Failed to set service level")?;
Ok(manager)
}
fn service_label() -> ServiceLabel {
ServiceLabel {
qualifier: Some("com.github".to_string()),
organization: Some("torc".to_string()),
application: SERVICE_NAME.to_string(),
}
}
pub fn install_service(config: &ServiceConfig, user_level: bool) -> Result<()> {
if config.https {
if config.tls_cert.is_none() {
anyhow::bail!("--https requires --tls-cert to be specified");
}
if config.tls_key.is_none() {
anyhow::bail!("--https requires --tls-key to be specified");
}
}
let manager = get_service_manager(user_level)?;
let exe_path = env::current_exe().context("Failed to get current executable path")?;
let mut args: Vec<OsString> = vec!["run".into()];
if let Some(ref log_dir) = config.log_dir {
args.push("--log-dir".into());
args.push(log_dir.as_os_str().to_owned());
}
if let Some(ref database) = config.database {
args.push("--database".into());
args.push(database.into());
}
args.push("--host".into());
args.push(config.host.clone().into());
args.push("--port".into());
args.push(config.port.to_string().into());
args.push("--threads".into());
args.push(config.threads.to_string().into());
args.push("--log-level".into());
args.push(config.log_level.clone().into());
if config.json_logs {
args.push("--json-logs".into());
}
if let Some(ref auth_file) = config.auth_file {
args.push("--auth-file".into());
args.push(auth_file.into());
}
if config.require_auth {
args.push("--require-auth".into());
}
if config.credential_cache_ttl_secs != ServiceConfig::DEFAULT_CREDENTIAL_CACHE_TTL_SECS {
args.push("--credential-cache-ttl-secs".into());
args.push(config.credential_cache_ttl_secs.to_string().into());
}
if config.enforce_access_control {
args.push("--enforce-access-control".into());
}
if config.https {
args.push("--https".into());
}
if let Some(ref tls_cert) = config.tls_cert {
args.push("--tls-cert".into());
args.push(tls_cert.into());
}
if let Some(ref tls_key) = config.tls_key {
args.push("--tls-key".into());
args.push(tls_key.into());
}
for admin_user in &config.admin_users {
args.push("--admin-user".into());
args.push(admin_user.into());
}
let interval = config
.completion_check_interval_secs
.unwrap_or(ServiceConfig::DEFAULT_SERVICE_INTERVAL_SECS);
args.push("--completion-check-interval-secs".into());
args.push(interval.to_string().into());
let install_ctx = ServiceInstallCtx {
label: service_label(),
program: exe_path,
args,
contents: None, username: None, working_directory: None,
environment: None,
autostart: true, };
manager
.install(install_ctx)
.context("Failed to install service")?;
let service_type = if user_level { "user" } else { "system" };
println!(
"✓ Service '{}' installed successfully as {} service",
SERVICE_NAME, service_type
);
println!();
println!("Configuration:");
if let Some(ref log_dir) = config.log_dir {
println!(" Log directory: {}", log_dir.display());
}
if let Some(ref database) = config.database {
println!(" Database: {}", database);
}
println!(" Listen address: {}:{}", config.host, config.port);
println!(" HTTPS: {}", config.https);
println!(" Worker threads: {}", config.threads);
println!(" Log level: {}", config.log_level);
println!(" Require auth: {}", config.require_auth);
println!(
" Enforce access control: {}",
config.enforce_access_control
);
if !config.admin_users.is_empty() {
println!(" Admin users: {}", config.admin_users.join(", "));
}
println!();
println!("To start the service, run:");
if user_level {
println!(" torc-server service start --user");
} else {
println!(" sudo torc-server service start");
}
Ok(())
}
pub fn uninstall_service(user_level: bool) -> Result<()> {
let manager = get_service_manager(user_level)?;
manager
.uninstall(ServiceUninstallCtx {
label: service_label(),
})
.context("Failed to uninstall service")?;
let service_type = if user_level { "user" } else { "system" };
println!(
"✓ Service '{}' uninstalled successfully ({} service)",
SERVICE_NAME, service_type
);
Ok(())
}
pub fn start_service(user_level: bool) -> Result<()> {
let manager = get_service_manager(user_level)?;
let label = service_label();
manager
.start(ServiceStartCtx { label })
.context("Failed to start service")?;
let service_type = if user_level { "user" } else { "system" };
println!(
"✓ Service '{}' started successfully ({} service)",
SERVICE_NAME, service_type
);
Ok(())
}
pub fn stop_service(user_level: bool) -> Result<()> {
let manager = get_service_manager(user_level)?;
let label = service_label();
manager
.stop(ServiceStopCtx { label })
.context("Failed to stop service")?;
let service_type = if user_level { "user" } else { "system" };
println!(
"✓ Service '{}' stopped successfully ({} service)",
SERVICE_NAME, service_type
);
Ok(())
}
pub fn service_status(user_level: bool) -> Result<()> {
let service_type = if user_level { "user" } else { "system" };
println!(
"Service status check varies by platform ({} service):",
service_type
);
println!();
#[cfg(target_os = "linux")]
{
println!("On Linux (systemd):");
if user_level {
println!(" systemctl --user status com.github.torc.torc-server");
println!();
println!("Or check service logs:");
println!(" journalctl --user -u com.github.torc.torc-server -f");
} else {
println!(" sudo systemctl status com.github.torc.torc-server");
println!();
println!("Or check service logs:");
println!(" journalctl -u com.github.torc.torc-server -f");
}
}
#[cfg(target_os = "macos")]
{
println!("On macOS (launchd):");
if user_level {
println!(" launchctl list | grep torc-server");
println!();
println!("Or check service logs:");
println!(" tail -f ~/Library/Logs/torc-server.log");
} else {
println!(" sudo launchctl list | grep torc-server");
println!();
println!("Or check service logs:");
println!(" tail -f /var/log/torc/torc-server.log");
}
}
#[cfg(target_os = "windows")]
{
println!("On Windows:");
println!(" sc query torc-server");
println!();
println!("Or use Services management console (services.msc)");
}
Ok(())
}
pub fn execute_service_command(
command: ServiceCommand,
config: Option<&ServiceConfig>,
user_level: bool,
) -> Result<()> {
match command {
ServiceCommand::Install => {
let merged_config = if let Some(user_config) = config {
ServiceConfig::merge_with_defaults(user_config, user_level)
} else if user_level {
ServiceConfig::default_user()
} else {
ServiceConfig::default_system()
};
install_service(&merged_config, user_level)
}
ServiceCommand::Uninstall => uninstall_service(user_level),
ServiceCommand::Start => start_service(user_level),
ServiceCommand::Stop => stop_service(user_level),
ServiceCommand::Status => service_status(user_level),
}
}