use anyhow::{anyhow, Result};
use dirs::home_dir;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
const SYSTEMD_USER_DIR: &str = ".config/systemd/user";
fn service_file_path() -> Result<PathBuf> {
let home = home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
Ok(home.join(SYSTEMD_USER_DIR).join("netscli-mcp.service"))
}
fn get_binary_path() -> Result<String> {
if let Ok(exe) = std::env::current_exe() {
if let Some(s) = exe.to_str() {
return Ok(s.to_string());
}
}
let output = Command::new("which").arg("netscli").output()?;
if output.status.success() {
let path = String::from_utf8(output.stdout)?.trim().to_string();
if !path.is_empty() {
return Ok(path);
}
}
Ok("/usr/local/bin/netscli".to_string())
}
pub(crate) fn render_service_unit(binary_path: &str) -> String {
format!(
r#"[Unit]
Description=NetsCLI MCP Server
After=network.target
[Service]
Type=simple
ExecStart="{binary_path}" serve
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
Environment=RUST_LOG=info
[Install]
WantedBy=default.target
"#
)
}
pub fn install_service() -> Result<()> {
let service_path = service_file_path()?;
let binary_path = get_binary_path()?;
if let Some(parent) = service_path.parent() {
fs::create_dir_all(parent)?;
}
let service_content = render_service_unit(&binary_path);
fs::write(&service_path, service_content)?;
println!("â
Service file created at: {}", service_path.display());
println!("\nđ Next steps:");
println!(" 1. Reload systemd: systemctl --user daemon-reload");
println!(" 2. Enable service: systemctl --user enable netscli-mcp.service");
println!(" 3. Start service: systemctl --user start netscli-mcp.service");
println!(" 4. Check status: systemctl --user status netscli-mcp.service");
Ok(())
}
pub fn uninstall_service() -> Result<()> {
let service_path = service_file_path()?;
if service_path.exists() {
let _ = Command::new("systemctl")
.args(["--user", "stop", "netscli-mcp.service"])
.output();
let _ = Command::new("systemctl")
.args(["--user", "disable", "netscli-mcp.service"])
.output();
fs::remove_file(&service_path)?;
println!("â
Service file removed: {}", service_path.display());
} else {
println!("âšī¸ Service file not found: {}", service_path.display());
}
Ok(())
}
pub fn show_status() -> Result<()> {
let service_path = service_file_path()?;
println!("Service file: {}", service_path.display());
if service_path.exists() {
println!("Status: â
Installed");
let output = Command::new("systemctl")
.args(["--user", "status", "netscli-mcp.service", "--no-pager"])
.output();
if let Ok(out) = output {
if out.status.success() {
println!("\nSystemd status:");
println!("{}", String::from_utf8_lossy(&out.stdout));
} else {
println!("\nâ ī¸ Service may not be enabled/started yet.");
println!(" Run: systemctl --user enable netscli-mcp.service");
println!(" Run: systemctl --user start netscli-mcp.service");
}
}
} else {
println!("Status: â Not installed");
println!("\nđĄ Run `netscli mcp-service --install` to create the service file.");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::render_service_unit;
#[test]
fn unit_file_contains_required_sections() {
let unit = render_service_unit("/usr/local/bin/netscli");
assert!(unit.contains("[Unit]"));
assert!(unit.contains("[Service]"));
assert!(unit.contains("[Install]"));
assert!(unit.contains("WantedBy=default.target"));
}
#[test]
fn unit_file_quotes_binary_path() {
let unit = render_service_unit("/home/a b/.cargo/bin/netscli");
assert!(
unit.contains(r#"ExecStart="/home/a b/.cargo/bin/netscli" serve"#),
"expected quoted path with `serve` arg, got:\n{unit}"
);
}
#[test]
fn unit_file_sets_rust_log_env() {
let unit = render_service_unit("/usr/local/bin/netscli");
assert!(
unit.contains("Environment=RUST_LOG=info"),
"expected RUST_LOG=info so tracing output is visible in the journal"
);
}
#[test]
fn unit_file_restart_policy() {
let unit = render_service_unit("/usr/local/bin/netscli");
assert!(unit.contains("Restart=always"));
assert!(unit.contains("RestartSec=5"));
}
#[test]
fn unit_file_journal_logging() {
let unit = render_service_unit("/usr/local/bin/netscli");
assert!(unit.contains("StandardOutput=journal"));
assert!(unit.contains("StandardError=journal"));
}
}