use crate::logging::{log_error, log_info, log_success};
use crate::utils::command_exists;
use std::fs;
use std::path::PathBuf;
use tokio::process::Command;
use tracing::debug;
pub async fn install_api_service(port: u16, debug: bool) -> Result<(), String> {
if !cfg!(target_os = "linux") {
return Err("`xbp api install` is only supported on Linux/systemd hosts.".to_string());
}
if !command_exists("systemctl") {
return Err(
"`systemctl` was not found. Install systemd or manage the service manually."
.to_string(),
);
}
let exe = std::env::current_exe()
.map_err(|e| format!("Failed to resolve current executable: {}", e))?;
let exe_dir: PathBuf = exe
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let unit = format!(
"[Unit]\nDescription=XBP API Server\nAfter=network.target\nWants=network-online.target\n\n[Service]\nType=simple\nEnvironment=XBP_API_BIND=127.0.0.1\nEnvironment=PORT_XBP_API={port}\nEnvironmentFile=-/etc/default/xbp\nWorkingDirectory={}\nExecStart={}\nRestart=always\nRestartSec=5\nNoNewPrivileges=yes\nPrivateTmp=yes\nProtectSystem=full\nProtectHome=read-only\nReadWritePaths=/var/log /etc/nginx /etc/systemd/system /run\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_DAC_OVERRIDE CAP_CHOWN CAP_SETUID CAP_SETGID\n\n[Install]\nWantedBy=multi-user.target\n",
exe_dir.display(),
exe.display()
);
let unit_path = "/etc/systemd/system/xbp-api.service";
let temp_unit_path = "/tmp/xbp-api.service";
let _ = log_info("api", "Writing systemd unit (requires sudo)", None).await;
fs::write(temp_unit_path, unit).map_err(|e| {
format!(
"Failed to stage temporary unit file {}: {}",
temp_unit_path, e
)
})?;
let write_cmd = if should_use_sudo() {
format!("sudo install -m 0644 {} {}", temp_unit_path, unit_path)
} else {
format!("install -m 0644 {} {}", temp_unit_path, unit_path)
};
if debug {
debug!("Writing unit with: {}", write_cmd);
}
let write_output = Command::new("sh")
.arg("-c")
.arg(&write_cmd)
.output()
.await
.map_err(|e| format!("Failed to write unit file: {}", e))?;
if !write_output.status.success() {
return Err(format!(
"Writing unit failed: {}",
String::from_utf8_lossy(&write_output.stderr)
));
}
let _ = fs::remove_file(temp_unit_path);
run_systemctl(&["daemon-reload"], debug).await?;
let enable_status = run_systemctl(&["enable", "--now", "xbp-api.service"], debug).await?;
if !enable_status.success() {
let _ = log_error(
"api",
"Failed to enable xbp-api.service",
Some(&format!("{:?}", enable_status)),
)
.await;
return Err("systemctl enable failed".into());
}
let _ = log_success(
"api",
&format!(
"Installed and started xbp-api.service on PORT_XBP_API={}",
port
),
None,
)
.await;
Ok(())
}
fn should_use_sudo() -> bool {
is_root().map(|value| !value).unwrap_or(true)
}
fn is_root() -> Option<bool> {
if !cfg!(target_os = "linux") {
return None;
}
let uid = std::process::Command::new("id")
.arg("-u")
.output()
.ok()
.filter(|output| output.status.success())?;
let raw = String::from_utf8_lossy(&uid.stdout);
Some(raw.trim() == "0")
}
async fn run_systemctl(args: &[&str], debug: bool) -> Result<std::process::ExitStatus, String> {
let mut command = if should_use_sudo() {
if !command_exists("sudo") {
return Err(
"This action requires elevated privileges. Re-run as root or install `sudo`."
.to_string(),
);
}
let mut cmd = Command::new("sudo");
cmd.arg("systemctl");
for arg in args {
cmd.arg(arg);
}
cmd
} else {
let mut cmd = Command::new("systemctl");
for arg in args {
cmd.arg(arg);
}
cmd
};
if debug {
debug!("Running systemctl {:?}", args);
}
command
.status()
.await
.map_err(|e| format!("Failed to run systemctl {:?}: {}", args, e))
}