//! systemd service installation for the host agent (Linux). //! //! `corrosion-host-agent install` writes a systemd unit pointing at the current //! binary + config, reloads systemd, and enables + starts the service. //! `uninstall` reverses it. Windows SCM support is a follow-up; on non-Linux //! these return a clear "Linux only" error rather than silently doing nothing. //! //! The agent already handles SIGTERM (see main::wait_for_shutdown_signal), so a //! plain `Type=simple` unit gives systemd clean start/stop semantics. use anyhow::{bail, Result}; use std::path::Path; #[cfg(target_os = "linux")] use anyhow::Context; pub const SERVICE_NAME: &str = "corrosion-host-agent"; #[cfg(target_os = "linux")] const UNIT_PATH: &str = "/etc/systemd/system/corrosion-host-agent.service"; /// Render the systemd unit. Pure (no I/O) so it is unit-testable. pub fn unit_file_contents(exec_path: &str, config_path: &str) -> String { format!( "[Unit]\n\ Description=Corrosion Host Agent (multi-game ops runtime)\n\ Documentation=https://corrosionmgmt.com\n\ After=network-online.target\n\ Wants=network-online.target\n\ \n\ [Service]\n\ Type=simple\n\ ExecStart={exec} --config {cfg}\n\ Restart=on-failure\n\ RestartSec=5\n\ # The agent supervises game-server processes and their files, so it\n\ # needs broad filesystem access and runs as root by default.\n\ User=root\n\ \n\ [Install]\n\ WantedBy=multi-user.target\n", exec = exec_path, cfg = config_path, ) } #[cfg(target_os = "linux")] pub fn install(config_path: &Path) -> Result<()> { let exec = std::env::current_exe().context("resolving current executable path")?; let exec_str = exec.to_string_lossy(); let cfg_str = config_path.to_string_lossy(); let unit = unit_file_contents(&exec_str, &cfg_str); std::fs::write(UNIT_PATH, unit) .with_context(|| format!("writing {UNIT_PATH} (are you root?)"))?; println!("wrote {UNIT_PATH}"); run("systemctl", &["daemon-reload"])?; run("systemctl", &["enable", "--now", SERVICE_NAME])?; println!( "service '{SERVICE_NAME}' installed and started.\n \ status: systemctl status {SERVICE_NAME}\n \ logs: journalctl -u {SERVICE_NAME} -f" ); Ok(()) } #[cfg(target_os = "linux")] pub fn uninstall() -> Result<()> { // Best-effort stop+disable; don't fail if it isn't currently active. let _ = std::process::Command::new("systemctl") .args(["disable", "--now", SERVICE_NAME]) .status(); if Path::new(UNIT_PATH).exists() { std::fs::remove_file(UNIT_PATH) .with_context(|| format!("removing {UNIT_PATH} (are you root?)"))?; println!("removed {UNIT_PATH}"); } run("systemctl", &["daemon-reload"])?; println!("service '{SERVICE_NAME}' uninstalled."); Ok(()) } #[cfg(target_os = "linux")] fn run(cmd: &str, args: &[&str]) -> Result<()> { let status = std::process::Command::new(cmd) .args(args) .status() .with_context(|| format!("running {cmd} {}", args.join(" ")))?; if !status.success() { bail!("{cmd} {} failed with {status}", args.join(" ")); } Ok(()) } #[cfg(not(target_os = "linux"))] pub fn install(_config_path: &Path) -> Result<()> { bail!( "`install` is only supported on Linux (systemd). Windows SCM support is \ coming; for now run the agent directly or via your platform's service manager." ); } #[cfg(not(target_os = "linux"))] pub fn uninstall() -> Result<()> { bail!("`uninstall` is only supported on Linux (systemd)."); } #[cfg(test)] mod tests { use super::*; #[test] fn unit_contains_exec_config_and_install_target() { let u = unit_file_contents( "/usr/local/bin/corrosion-host-agent", "/etc/corrosion/agent.toml", ); assert!(u.contains( "ExecStart=/usr/local/bin/corrosion-host-agent --config /etc/corrosion/agent.toml" )); assert!(u.contains("Type=simple")); assert!(u.contains("Restart=on-failure")); assert!(u.contains("WantedBy=multi-user.target")); assert!(u.contains("After=network-online.target")); } }