use anyhow::Result;
use clap::Subcommand;
#[cfg(target_os = "macos")]
use colored::Colorize;
#[derive(Debug, Clone, Subcommand)]
pub enum ServiceAction {
Install,
Start,
Stop,
Logs,
}
#[cfg(target_os = "macos")]
pub const LAUNCHD_LABEL: &str = "com.trusty.memory";
pub fn handle_service(action: &ServiceAction) -> Result<()> {
#[cfg(target_os = "macos")]
{
match action {
ServiceAction::Install => service_install(),
ServiceAction::Start => service_start(),
ServiceAction::Stop => service_stop(),
ServiceAction::Logs => service_logs(),
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = action;
anyhow::bail!(
"`trusty-memory service` is not supported on this platform — \
use your distro's service manager (systemd, OpenRC, etc.) directly."
);
}
}
#[cfg(target_os = "macos")]
pub(crate) fn launchd_log_dir() -> Result<std::path::PathBuf> {
let data =
dirs::data_dir().ok_or_else(|| anyhow::anyhow!("could not resolve user data directory"))?;
let dir = data.join("trusty-memory").join("logs");
std::fs::create_dir_all(&dir)
.map_err(|e| anyhow::anyhow!("create log dir {}: {e}", dir.display()))?;
Ok(dir)
}
#[cfg(target_os = "macos")]
pub(crate) 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!["serve".to_string()],
log_dir,
keep_alive: KeepAlive::OnSuccess,
throttle_interval: 10,
env_vars: vec![],
}
}
#[cfg(target_os = "macos")]
fn current_exe() -> Result<std::path::PathBuf> {
std::env::current_exe().map_err(|e| anyhow::anyhow!("could not resolve current exe: {e}"))
}
#[cfg(target_os = "macos")]
fn service_install() -> Result<()> {
let exe = current_exe()?;
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()
);
println!(
" Logs: {}\n Start: {}",
log_dir.display().to_string().dimmed(),
"trusty-memory service start".cyan(),
);
Ok(())
}
#[cfg(target_os = "macos")]
fn service_start() -> Result<()> {
let exe = current_exe()?;
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
);
println!(
" Logs: {}\n Stop: {}",
log_dir.display().to_string().dimmed(),
"trusty-memory service stop".cyan(),
);
Ok(())
}
#[cfg(target_os = "macos")]
fn service_stop() -> Result<()> {
let exe = current_exe()?;
let log_dir = launchd_log_dir()?;
let cfg = build_launchd_config(exe, log_dir);
cfg.bootout()?;
println!(
"{} Unloaded {} (plist file preserved at {}).",
"✓".green(),
LAUNCHD_LABEL,
cfg.plist_path()?.display().to_string().dimmed()
);
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(),
"trusty-memory service start".cyan()
);
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(())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(target_os = "macos"))]
#[test]
fn handle_service_errors_on_unsupported_platform() {
for action in [
ServiceAction::Install,
ServiceAction::Start,
ServiceAction::Stop,
ServiceAction::Logs,
] {
let err = handle_service(&action).expect_err("must fail on non-macOS");
let msg = format!("{err}");
assert!(
msg.contains("not supported"),
"expected platform error, got: {msg}"
);
}
}
#[cfg(target_os = "macos")]
#[test]
fn build_launchd_config_uses_canonical_shape() {
use std::path::PathBuf;
use trusty_common::launchd::KeepAlive;
let cfg = build_launchd_config(
PathBuf::from("/usr/local/bin/trusty-memory"),
PathBuf::from("/tmp/trusty-memory/logs"),
);
assert_eq!(cfg.label, LAUNCHD_LABEL);
assert_eq!(cfg.args, vec!["serve".to_string()]);
assert_eq!(cfg.keep_alive, KeepAlive::OnSuccess);
assert_eq!(cfg.throttle_interval, 10);
assert!(cfg.env_vars.is_empty());
}
}