use anyhow::Result;
use colored::Colorize;
#[cfg(target_os = "macos")]
const LAUNCHD_LABEL: &str = "com.trusty.trusty-analyze";
#[derive(Debug, Clone)]
pub enum ServiceAction {
Install,
Uninstall,
Status,
Logs,
}
pub fn run_service_action(action: ServiceAction) -> Result<()> {
#[cfg(target_os = "macos")]
{
match action {
ServiceAction::Install => service_install(),
ServiceAction::Uninstall => service_uninstall(),
ServiceAction::Status => service_status(),
ServiceAction::Logs => service_logs(),
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = action;
eprintln!(
"{} `trusty-analyze service` is not supported on this platform — \
use your distro's service manager (systemd, OpenRC, etc.) directly.",
"✗".red()
);
std::process::exit(1);
}
}
#[cfg(target_os = "macos")]
pub fn launchd_plist_path() -> Result<std::path::PathBuf> {
let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("could not resolve $HOME"))?;
Ok(home
.join("Library")
.join("LaunchAgents")
.join(format!("{LAUNCHD_LABEL}.plist")))
}
#[cfg(target_os = "macos")]
fn launchd_log_dir() -> Result<std::path::PathBuf> {
let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("could not resolve $HOME"))?;
let dir = home.join(".trusty-analyze").join("logs");
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
#[cfg(target_os = "macos")]
fn launchd_plist_body(exe: &std::path::Path, log_dir: &std::path::Path) -> String {
let exe = exe.display();
let data_dir = log_dir
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| log_dir.to_path_buf());
let log_path = log_dir.join("daemon.log");
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{LAUNCHD_LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>{exe}</string>
<string>serve</string>
</array>
<key>WorkingDirectory</key>
<string>{data}</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>{log}</string>
<key>StandardErrorPath</key>
<string>{log}</string>
<key>ThrottleInterval</key>
<integer>10</integer>
</dict>
</plist>
"#,
data = data_dir.display(),
log = log_path.display(),
)
}
#[cfg(target_os = "macos")]
pub fn service_install() -> Result<()> {
let exe = std::env::current_exe()
.map_err(|e| anyhow::anyhow!("could not resolve current exe: {e}"))?;
let plist_path = launchd_plist_path()?;
if let Some(parent) = plist_path.parent() {
std::fs::create_dir_all(parent)?;
}
let log_dir = launchd_log_dir()?;
let body = launchd_plist_body(&exe, &log_dir);
std::fs::write(&plist_path, body)
.map_err(|e| anyhow::anyhow!("write {}: {e}", plist_path.display()))?;
println!(
"{} Wrote LaunchAgent plist: {}",
"✓".green(),
plist_path.display()
);
let uid = unsafe { libc::getuid() };
let domain = format!("gui/{uid}");
let _ = std::process::Command::new("launchctl")
.args(["bootout", &domain])
.arg(&plist_path)
.status();
let status = std::process::Command::new("launchctl")
.args(["bootstrap", &domain])
.arg(&plist_path)
.status()
.map_err(|e| anyhow::anyhow!("launchctl bootstrap failed: {e}"))?;
if !status.success() {
anyhow::bail!("launchctl bootstrap exited with {status}");
}
println!(
"{} trusty-analyze service installed and started ({} loaded into {}).",
"✓".green(),
LAUNCHD_LABEL,
domain
);
println!(
" Logs: {}\n Status: {}",
log_dir.display().to_string().dimmed(),
"trusty-analyze service status".cyan(),
);
Ok(())
}
#[cfg(target_os = "macos")]
fn service_uninstall() -> Result<()> {
let plist_path = launchd_plist_path()?;
let uid = unsafe { libc::getuid() };
let domain = format!("gui/{uid}");
if plist_path.exists() {
let _ = std::process::Command::new("launchctl")
.args(["bootout", &domain])
.arg(&plist_path)
.status();
std::fs::remove_file(&plist_path)
.map_err(|e| anyhow::anyhow!("remove {}: {e}", plist_path.display()))?;
println!(
"{} trusty-analyze service uninstalled ({} removed).",
"✓".green(),
plist_path.display()
);
} else {
println!(
"{} {} not installed — nothing to do",
"·".dimmed(),
plist_path.display()
);
}
Ok(())
}
#[cfg(target_os = "macos")]
fn service_status() -> Result<()> {
let uid = unsafe { libc::getuid() };
let target = format!("gui/{uid}/{LAUNCHD_LABEL}");
let output = std::process::Command::new("launchctl")
.args(["print", &target])
.output()
.map_err(|e| anyhow::anyhow!("launchctl print failed: {e}"))?;
if output.status.success() {
println!("{}", String::from_utf8_lossy(&output.stdout));
} else {
eprintln!(
"{} {} is not loaded ({})",
"✗".red(),
target,
String::from_utf8_lossy(&output.stderr).trim()
);
eprintln!(
" Install with: {}",
"trusty-analyze service install".cyan()
);
std::process::exit(1);
}
Ok(())
}
#[cfg(target_os = "macos")]
fn service_logs() -> Result<()> {
use std::os::unix::process::CommandExt;
let log_dir = launchd_log_dir()?;
let log = log_dir.join("daemon.log");
if !log.exists() {
eprintln!(
"{} No log at {} yet — start the service first.",
"·".dimmed(),
log.display()
);
return Ok(());
}
let err = std::process::Command::new("tail")
.arg("-F")
.arg(&log)
.exec();
Err(anyhow::anyhow!("exec tail failed: {err}"))
}