use anyhow::Result;
use clap::Subcommand;
#[cfg(target_os = "macos")]
use colored::Colorize;
#[derive(Debug, Clone, Subcommand)]
pub enum ServiceAction {
Install,
Uninstall,
Status,
Logs,
}
#[cfg(target_os = "macos")]
const LAUNCHD_LABEL: &str = "com.trusty.trusty-search";
pub fn handle_service(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;
anyhow::bail!(
"`trusty-search service` is not supported on this platform — \
use your distro's service manager (systemd, OpenRC, etc.) directly."
);
}
}
#[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("Library").join("Logs").join("trusty-search");
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
#[cfg(target_os = "macos")]
fn launchd_env_vars() -> Vec<(String, String)> {
use crate::service::PERSISTED_ENV_VARS;
let mut pairs: Vec<(String, String)> = Vec::new();
if let Some(home) = dirs::home_dir() {
let hf_home = home.join(".cache").join("huggingface");
pairs.push(("HF_HOME".to_string(), hf_home.display().to_string()));
}
for key in PERSISTED_ENV_VARS {
if let Ok(val) = std::env::var(key) {
pairs.push((key.to_string(), val));
}
}
pairs
}
#[cfg(target_os = "macos")]
fn build_launchd_config(
exe: std::path::PathBuf,
log_dir: std::path::PathBuf,
) -> trusty_common::launchd::LaunchdConfig {
use trusty_common::launchd::{KeepAlive, LaunchdConfig};
LaunchdConfig {
label: LAUNCHD_LABEL.to_string(),
exe_path: exe,
args: vec!["start".to_string(), "--foreground".to_string()],
log_dir,
keep_alive: KeepAlive::OnSuccess,
throttle_interval: 30,
env_vars: launchd_env_vars(),
}
}
#[cfg(target_os = "macos")]
fn service_install() -> Result<()> {
let exe = std::env::current_exe()
.map_err(|e| anyhow::anyhow!("could not resolve current exe: {e}"))?;
let log_dir = launchd_log_dir()?;
let cfg = build_launchd_config(exe, log_dir.clone());
let plist_path = cfg.plist_path()?;
cfg.install()?;
println!(
"{} Wrote LaunchAgent plist: {}",
"✓".green(),
plist_path.display()
);
cfg.bootstrap()?;
let domain = format!("gui/{}", trusty_common::launchd::current_uid());
println!(
"{} Loaded {} into {} — daemon will start automatically.",
"✓".green(),
LAUNCHD_LABEL,
domain
);
match crate::commands::log_rotation::install_rotation() {
Ok(()) => println!(
"{} Installed stderr.log rotation (1 MB × 7 archives, daily check)",
"✓".green()
),
Err(e) => eprintln!(
"{} Could not install log rotation ({e}) — run `trusty-search doctor --fix` later",
"⚠".yellow()
),
}
println!(
" Logs: {}\n Status: {}",
log_dir.display().to_string().dimmed(),
"trusty-search service status".cyan(),
);
Ok(())
}
#[cfg(target_os = "macos")]
fn service_uninstall() -> Result<()> {
let exe = std::env::current_exe()
.map_err(|e| anyhow::anyhow!("could not resolve current exe: {e}"))?;
let log_dir = launchd_log_dir()?;
let cfg = build_launchd_config(exe, log_dir);
let plist_path = cfg.plist_path()?;
let uid = trusty_common::launchd::current_uid();
let domain = format!("gui/{uid}");
if plist_path.exists() {
let _ = cfg.bootout();
std::fs::remove_file(&plist_path)
.map_err(|e| anyhow::anyhow!("remove {}: {e}", plist_path.display()))?;
println!(
"{} Unloaded and removed {}",
"✓".green(),
plist_path.display()
);
if let Ok(rot_plist) = crate::commands::log_rotation::rotation_plist_path() {
if rot_plist.exists() {
let _ = std::process::Command::new("launchctl")
.args(["bootout", &domain])
.arg(&rot_plist)
.status();
let _ = std::fs::remove_file(&rot_plist);
}
}
if let Ok(conf) = crate::commands::log_rotation::newsyslog_conf_path() {
let _ = std::fs::remove_file(&conf);
}
} else {
println!(
"{} {} not installed — nothing to do",
"·".dimmed(),
plist_path.display()
);
}
Ok(())
}
#[cfg(target_os = "macos")]
fn service_status() -> Result<()> {
let uid = trusty_common::launchd::current_uid();
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!(" Install with: trusty-search service install");
anyhow::bail!(
"{} is not loaded ({})",
target,
String::from_utf8_lossy(&output.stderr).trim()
);
}
Ok(())
}
#[cfg(target_os = "macos")]
fn service_logs() -> Result<()> {
let log_dir = launchd_log_dir()?;
let stdout = log_dir.join("stdout.log");
let stderr = log_dir.join("stderr.log");
if !stdout.exists() && !stderr.exists() {
eprintln!(
"{} No logs at {} yet — start the service first.",
"·".dimmed(),
log_dir.display()
);
return Ok(());
}
let status = std::process::Command::new("tail")
.arg("-F")
.arg(&stdout)
.arg(&stderr)
.status()
.map_err(|e| anyhow::anyhow!("tail failed: {e}"))?;
if !status.success() {
anyhow::bail!("tail exited with {status}");
}
Ok(())
}