use anyhow::Context as _;
#[cfg(target_os = "macos")]
pub(crate) const MAIN_DAEMON_PLIST_LABEL: &str = trusty_common::launchd_labels::MPM;
#[cfg(target_os = "macos")]
fn plist_path_in(home: &std::path::Path, label: &str) -> std::path::PathBuf {
home.join("Library")
.join("LaunchAgents")
.join(format!("{label}.plist"))
}
#[cfg(target_os = "macos")]
fn main_daemon_managed_by_launchd_in(home: &std::path::Path) -> bool {
plist_path_in(home, MAIN_DAEMON_PLIST_LABEL).exists()
}
fn autostart_pidfile_path(root: &std::path::Path) -> std::path::PathBuf {
root.join("autostart-daemon.pid")
}
fn write_autostart_pidfile(
root: &std::path::Path,
pid: u32,
) -> std::io::Result<std::path::PathBuf> {
let path = autostart_pidfile_path(root);
std::fs::write(&path, pid.to_string())?;
Ok(path)
}
pub(crate) fn remove_autostart_pidfile(root: &std::path::Path) {
let _ = std::fs::remove_file(autostart_pidfile_path(root));
}
#[cfg(target_os = "macos")]
enum LaunchctlOutcome {
Launched,
AlreadyLoaded,
Unavailable,
}
pub(crate) async fn ensure_daemon_started(
client: &reqwest::Client,
_url: &str,
) -> anyhow::Result<String> {
#[cfg(target_os = "macos")]
let needs_spawn = {
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
if main_daemon_managed_by_launchd_in(&home) {
let plist_path = plist_path_in(&home, MAIN_DAEMON_PLIST_LABEL);
matches!(
try_launchctl_start(&plist_path),
LaunchctlOutcome::Unavailable
)
} else {
true
}
};
#[cfg(not(target_os = "macos"))]
let needs_spawn = true;
let mut spawned_root: Option<std::path::PathBuf> = None;
if needs_spawn {
let lock_path = trusty_mpm::core::lock_file_path();
let _ = std::fs::remove_file(&lock_path);
let root = trusty_mpm::core::paths::FrameworkPaths::default().root;
std::fs::create_dir_all(&root).context("create framework dir for daemon log")?;
let log_path = root.join("daemon.log");
let log_file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.context("open daemon log file")?;
let log_copy = log_file.try_clone().context("clone log file handle")?;
let exe = std::env::current_exe().context("resolve current executable path")?;
let child = std::process::Command::new(&exe)
.arg("daemon")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::from(log_file))
.stderr(std::process::Stdio::from(log_copy))
.spawn()
.context("spawn daemon process")?;
if let Err(e) = write_autostart_pidfile(&root, child.id()) {
eprintln!("tm: warning: could not write autostart pidfile: {e}");
}
spawned_root = Some(root);
drop(child);
}
for _ in 0..10 {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let resolved = trusty_mpm::core::resolve_daemon_url(None);
if super::daemon::daemon_healthy(client, &resolved).await {
eprintln!("tm: daemon ready");
return Ok(resolved);
}
}
if let Some(root) = spawned_root.as_deref() {
remove_autostart_pidfile(root);
}
anyhow::bail!("daemon did not become healthy within 5 s after auto-start")
}
#[cfg(target_os = "macos")]
fn try_launchctl_start(plist_path: &std::path::Path) -> LaunchctlOutcome {
if !plist_path.exists() {
return LaunchctlOutcome::Unavailable;
}
let uid = std::process::Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_default();
if uid.is_empty() {
return LaunchctlOutcome::Unavailable;
}
let domain = format!("gui/{uid}");
let plist_str = match plist_path.to_str() {
Some(s) => s,
None => return LaunchctlOutcome::Unavailable,
};
let output = match std::process::Command::new("launchctl")
.args(["bootstrap", &domain, plist_str])
.output()
{
Ok(o) => o,
Err(_) => return LaunchctlOutcome::Unavailable,
};
if output.status.success() {
return LaunchctlOutcome::Launched;
}
let stderr = String::from_utf8_lossy(&output.stderr);
if output.status.code() == Some(37)
|| stderr.contains("already bootstrapped")
|| stderr.contains("service already loaded")
{
return LaunchctlOutcome::AlreadyLoaded;
}
LaunchctlOutcome::Unavailable
}
pub(crate) fn github_host(lower_url: &str) -> &str {
if let Some(at) = lower_url.find('@') {
let after = &lower_url[at + 1..];
if let Some(col) = after.find(':') {
return &after[..col];
}
}
let without_scheme = lower_url
.find("://")
.map(|i| &lower_url[i + 3..])
.unwrap_or(lower_url);
let after_userinfo = without_scheme
.find('@')
.map(|i| &without_scheme[i + 1..])
.unwrap_or(without_scheme);
let host_with_port = after_userinfo.split('/').next().unwrap_or(after_userinfo);
if let Some(colon) = host_with_port.rfind(':') {
let port_str = &host_with_port[colon + 1..];
if !port_str.is_empty() && port_str.chars().all(|c| c.is_ascii_digit()) {
return &host_with_port[..colon];
}
}
host_with_port
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "macos")]
#[test]
fn main_daemon_plist_path_uses_main_label() {
let home = std::path::Path::new("/tmp/fake-home");
let path = plist_path_in(home, MAIN_DAEMON_PLIST_LABEL);
assert_eq!(MAIN_DAEMON_PLIST_LABEL, "com.trusty.mpm");
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
assert_eq!(
filename, "com.trusty.mpm.plist",
"must target the main daemon plist, not the supervisor"
);
let parent = path
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.unwrap_or("");
assert_eq!(
parent, "LaunchAgents",
"plist must live under Library/LaunchAgents/"
);
}
#[cfg(target_os = "macos")]
#[test]
fn main_daemon_managed_by_launchd() {
let tmp = tempfile::tempdir().expect("create temp home");
let home = tmp.path();
assert!(
!main_daemon_managed_by_launchd_in(home),
"empty home must not report launchd management"
);
let agents = plist_path_in(home, MAIN_DAEMON_PLIST_LABEL);
std::fs::create_dir_all(agents.parent().expect("agents dir"))
.expect("create LaunchAgents dir");
std::fs::write(&agents, "<plist/>").expect("write main plist");
assert!(
main_daemon_managed_by_launchd_in(home),
"installed main-daemon plist must report launchd management"
);
}
#[cfg(target_os = "macos")]
#[test]
fn main_daemon_managed_ignores_supervisor_only_home() {
let tmp = tempfile::tempdir().expect("create temp home");
let home = tmp.path();
let supervisor = plist_path_in(home, "com.trusty.mpm.supervisor");
std::fs::create_dir_all(supervisor.parent().expect("agents dir"))
.expect("create LaunchAgents dir");
std::fs::write(&supervisor, "<plist/>").expect("write supervisor plist");
assert!(
!main_daemon_managed_by_launchd_in(home),
"a supervisor-only home must not report the main daemon as managed (#1900)"
);
}
#[test]
fn autostart_pidfile_roundtrip() {
let tmp = tempfile::tempdir().expect("create temp root");
let root = tmp.path();
let path = write_autostart_pidfile(root, 424242).expect("write pidfile");
assert_eq!(path, autostart_pidfile_path(root));
let contents = std::fs::read_to_string(&path).expect("read pidfile");
assert_eq!(contents, "424242", "pidfile must contain the child PID");
remove_autostart_pidfile(root);
assert!(
!path.exists(),
"remove_autostart_pidfile must delete the pidfile"
);
}
#[test]
fn github_host_strips_port_from_https_url() {
assert_eq!(
github_host("https://github.com:443/owner/repo.git"),
"github.com",
"port suffix must be stripped from https:// URLs"
);
assert_eq!(
github_host("ssh://git@github-work:2222/o/r.git"),
"github-work",
"port suffix must be stripped from ssh:// URLs with userinfo"
);
assert_eq!(
github_host("git@github.com:owner/repo.git"),
"github.com",
"scp-style host must be unaffected"
);
}
}