use anyhow::Result;
use clap::Subcommand;
#[derive(Debug, Clone, Subcommand)]
pub enum ServiceAction {
Install,
Uninstall,
Status,
Logs,
}
#[cfg(target_os = "macos")]
pub const LAUNCHD_LABEL: &str = trusty_common::launchd_labels::CONSOLE;
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;
anyhow::bail!(
"`trusty-console service` is only supported on macOS — \
use your distro's service manager (systemd, OpenRC, etc.) directly."
);
}
}
#[cfg(target_os = "macos")]
fn serve_args() -> Vec<String> {
vec!["serve".to_string()]
}
#[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-console").join("logs");
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
#[cfg(target_os = "macos")]
fn launchd_config() -> Result<trusty_common::launchd::LaunchdConfig> {
use trusty_common::launchd::{KeepAlive, LaunchdConfig};
let exe = std::env::current_exe()
.map_err(|e| anyhow::anyhow!("could not resolve current exe: {e}"))?;
let log_dir = launchd_log_dir()?;
Ok(LaunchdConfig {
label: LAUNCHD_LABEL.to_string(),
exe_path: exe,
args: serve_args(),
log_dir,
keep_alive: KeepAlive::Always,
throttle_interval: 10,
env_vars: Vec::new(),
fd_limit: None,
working_directory: None,
}
.with_daemon_path())
}
#[cfg(target_os = "macos")]
fn service_install() -> Result<()> {
let cfg = launchd_config()?;
let plist_path = cfg.plist_path()?;
let outcome = cfg
.install_and_activate(trusty_common::launchd_labels::legacy_labels_for(
LAUNCHD_LABEL,
))
.map_err(|e| anyhow::anyhow!("install LaunchAgent: {e}"))?;
for label in outcome.evicted() {
println!(
"[warn] Evicted the stale LaunchAgent {label} — it named this daemon under an old label."
);
}
let domain = format!("gui/{}", trusty_common::launchd::current_uid());
if matches!(
outcome,
trusty_common::launchd_activate::Activation::AlreadyCurrent { .. }
) {
println!(
"[ok] {LAUNCHD_LABEL} is already loaded in {domain} with this exact unit — left running."
);
println!(
" Logs: {}\n Status: trusty-console service status",
cfg.log_dir.display()
);
return Ok(());
}
println!("[ok] Wrote LaunchAgent plist: {}", plist_path.display());
println!(
"[ok] trusty-console service installed and started ({LAUNCHD_LABEL} loaded into {domain})."
);
println!(
" Logs: {}\n Status: trusty-console service status",
cfg.log_dir.display()
);
Ok(())
}
#[cfg(target_os = "macos")]
fn service_uninstall() -> Result<()> {
let cfg = launchd_config()?;
let plist_path = cfg.plist_path()?;
for e in cfg.evict_legacy_detailed(trusty_common::launchd_labels::legacy_labels_for(
LAUNCHD_LABEL,
)) {
use trusty_common::launchd_labels::EvictionOutcome;
match &e.outcome {
EvictionOutcome::Evicted => {
println!(
"[ok] Unloaded and removed the stale LaunchAgent {}",
e.label
);
}
EvictionOutcome::Failed(why) => {
eprintln!(
"[!!] Could not clear the stale LaunchAgent {}: {why}",
e.label
);
}
_ => {}
}
}
if plist_path.exists() {
let _ = cfg.bootout();
std::fs::remove_file(&plist_path)
.map_err(|e| anyhow::anyhow!("remove {}: {e}", plist_path.display()))?;
println!(
"[ok] trusty-console service uninstalled ({} removed).",
plist_path.display()
);
} else {
println!(
"[skip] {} not installed — nothing to do",
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));
Ok(())
} else {
eprintln!(" Install with: trusty-console service install");
anyhow::bail!(
"{target} is not loaded ({})",
String::from_utf8_lossy(&output.stderr).trim()
);
}
}
#[cfg(target_os = "macos")]
fn service_logs() -> Result<()> {
let log_dir = launchd_log_dir()?;
let stdout_log = log_dir.join("stdout.log");
let stderr_log = log_dir.join("stderr.log");
if !stdout_log.exists() && !stderr_log.exists() {
eprintln!(
"[skip] No logs at {} yet — start the service first.",
log_dir.display()
);
return Ok(());
}
let status = std::process::Command::new("tail")
.arg("-F")
.arg(&stdout_log)
.arg(&stderr_log)
.status()
.map_err(|e| anyhow::anyhow!("tail failed: {e}"))?;
if !status.success() {
anyhow::bail!("tail exited with {status}");
}
Ok(())
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
#[test]
fn service_label_matches_tctl_convention() {
assert_eq!(LAUNCHD_LABEL, trusty_common::launchd_labels::CONSOLE);
assert_ne!(
LAUNCHD_LABEL, "com.trusty.trusty-console",
"the full-name form is a legacy alias, not a unit launchd has"
);
}
#[test]
fn serve_args_are_serve() {
assert_eq!(serve_args(), vec!["serve".to_string()]);
}
#[test]
fn default_port_does_not_collide_with_known_siblings() {
let known_siblings: &[(&str, u16, &str)] = &[
(
"trusty-search",
7878,
"trusty-search/src/service/constants.rs::DEFAULT_PORT",
),
(
"trusty-mpm",
7880,
"trusty-mpm/src/core/discovery.rs::DEFAULT_DAEMON_ADDR",
),
(
"trusty-embedderd",
7890,
"trusty-embedderd/src/lib.rs::Args::http_addr (--http default_value, manual/dev-run only)",
),
(
"trusty-agents",
8080,
"trusty-agents/src/runtime/mode_dispatch.rs (--port default 8080)",
),
(
"trusty-code",
7882,
"trusty-code/src/serve/mod.rs::DEFAULT_HTTP_PORT",
),
];
for (binary, port, source) in known_siblings {
assert_ne!(
crate::DEFAULT_PORT,
*port,
"trusty-console DEFAULT_PORT {} collides with {binary}'s {port} ({source})",
crate::DEFAULT_PORT
);
}
}
}