use std::net::SocketAddr;
pub(super) const ALREADY_RUNNING_NOTICE: &str = "the trusty-mpm daemon is already running";
pub(crate) async fn run_daemon(
addr: SocketAddr,
tailscale: bool,
mcp: bool,
force: bool,
sandbox: bool,
) -> anyhow::Result<()> {
use std::io::ErrorKind;
if tailscale {
return Err(tailscale_deprecated_error());
}
if sandbox {
crate::commands::daemon_sandbox::enter()?;
}
if let Some(home) = dirs::home_dir() {
let _ = std::env::set_current_dir(&home);
} else {
let _ = std::env::set_current_dir("/");
}
let state = trusty_mpm::daemon::DaemonState::shared();
let supervised = apply_supervision_signal(&state);
state.set_build_identity(trusty_mpm::core::build_identity::build_id());
state.set_unsupervised_forced(force);
if crate::commands::launchd_probe::compute_daemon_refuse(supervised, force) {
return Err(unsupervised_daemon_error());
}
if mcp {
return trusty_mpm::daemon::run_mcp(state).await;
}
let recorded_url = trusty_mpm::core::resolve_daemon_url(None);
if recorded_url != trusty_mpm::core::DEFAULT_DAEMON_URL
&& trusty_common::probe_health(&recorded_url, "/health").await
{
tracing::debug!("daemon already running at recorded url {recorded_url}");
eprintln!("{ALREADY_RUNNING_NOTICE}");
return Ok(());
}
let intended_url = format!("http://{addr}");
if trusty_common::probe_health(&intended_url, "/health").await {
tracing::debug!("daemon already running at intended url {intended_url}");
eprintln!("{ALREADY_RUNNING_NOTICE}");
return Ok(());
}
let listener = match tokio::net::TcpListener::bind(addr).await {
Ok(l) => l,
Err(e) if e.kind() == ErrorKind::AddrInUse => {
tracing::warn!("port {} is busy — selecting an ephemeral port", addr.port());
tokio::net::TcpListener::bind("127.0.0.1:0").await?
}
Err(e) => return Err(e.into()),
};
let actual_addr = listener.local_addr()?;
let base_url = format!("http://{actual_addr}");
tracing::info!("daemon listening on {base_url}");
let socket_path = trusty_mpm::daemon::socket::socket_path()?;
let rpc = bind_socket_then_publish(
&base_url,
&socket_path,
&trusty_mpm::core::discovery::lock_file_path(),
)
.await?;
if let Err(e) = trusty_common::write_daemon_addr("trusty-mpm", &actual_addr.to_string()) {
tracing::warn!("failed to write trusty-mpm http_addr discovery file: {e:#}");
}
let bot_shutdown = crate::commands::daemon_sandbox::gate_channel_pollers(sandbox, || {
spawn_telegram_bot(&base_url)
});
tokio::spawn(async move {
wait_for_shutdown_signal().await;
if let Some(token) = bot_shutdown {
token.cancel();
}
trusty_mpm::daemon::lock::remove_lock();
if let Err(e) = trusty_common::remove_daemon_addr("trusty-mpm") {
tracing::warn!("failed to remove trusty-mpm http_addr discovery file: {e:#}");
}
std::process::exit(0);
});
trusty_mpm::daemon::serve_http(state, listener, rpc).await
}
async fn bind_socket_then_publish(
base_url: &str,
socket: &std::path::Path,
lock_path: &std::path::Path,
) -> anyhow::Result<trusty_mpm::daemon::socket::BoundSocket> {
let rpc = trusty_mpm::daemon::socket::bind(socket).await?;
trusty_mpm::core::daemon_identity::write_lock_at(
lock_path,
base_url,
&socket.display().to_string(),
);
Ok(rpc)
}
async fn wait_for_shutdown_signal() {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
let mut term = match signal(SignalKind::terminate()) {
Ok(s) => s,
Err(e) => {
tracing::warn!("failed to register SIGTERM handler: {e}");
let _ = tokio::signal::ctrl_c().await;
return;
}
};
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = term.recv() => {}
}
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
}
}
fn apply_supervision_signal(state: &trusty_mpm::daemon::DaemonState) -> bool {
use trusty_common::supervision::{LaunchdSupervision, launchd_supervision};
let plist_exists = crate::commands::launchd_probe::launchd_may_own_daemon();
let answer = launchd_supervision();
let discriminant = match &answer {
LaunchdSupervision::Supervised(_) => "supervised",
LaunchdSupervision::NotSupervised => "not_supervised",
LaunchdSupervision::Unknown(_) => "unknown",
};
state.set_launchd_supervision(discriminant);
if let LaunchdSupervision::Unknown(why) = &answer {
tracing::warn!(
reason = %why,
"could not determine launchd supervision authoritatively — /health will \
report launchd_supervision=unknown (issue #4469)"
);
}
let is_launchd = answer.is_supervised();
let supervised = crate::commands::launchd_probe::compute_supervised(is_launchd, plist_exists);
state.set_supervised(supervised);
if !supervised {
tracing::warn!(
"unsupervised trusty-mpm daemon running alongside a launchd unit — \
likely spawned by a client during a restart race; kill it and use \
`launchctl kickstart -k gui/$(id -u)/{supervisor}` (or `{daemon}`) \
to let launchd own the daemon.",
supervisor = trusty_common::launchd_labels::MPM_SUPERVISOR,
daemon = trusty_common::launchd_labels::MPM,
);
}
supervised
}
fn unsupervised_daemon_error() -> anyhow::Error {
anyhow::anyhow!(
"refusing to start: a trusty-mpm launchd unit is registered but this \
process was not started by launchd (issue #2486/#4397) — starting \
anyway would create a duplicate, unsupervised daemon that can hold a \
stale binary indefinitely. Use \
`launchctl kickstart -k gui/$(id -u)/{supervisor}` (or `{daemon}`) to \
let launchd own the daemon, or pass `tm daemon --force` to start \
unsupervised anyway.",
supervisor = trusty_common::launchd_labels::MPM_SUPERVISOR,
daemon = trusty_common::launchd_labels::MPM,
)
}
fn spawn_telegram_bot(base_url: &str) -> Option<tokio_util::sync::CancellationToken> {
match trusty_mpm::telegram::resolve_token("TELEGRAM_BOT_TOKEN") {
Some(token) => {
tracing::info!("TELEGRAM_BOT_TOKEN found — starting supervised Telegram bot");
let url = base_url.to_string();
let shutdown = tokio_util::sync::CancellationToken::new();
let bot_shutdown = shutdown.clone();
tokio::spawn(async move {
let options = trusty_mpm::telegram::BotOptions::default();
trusty_mpm::telegram::run_supervised(url, Some(token), options, bot_shutdown).await;
});
Some(shutdown)
}
None => {
tracing::warn!(
"TELEGRAM_BOT_TOKEN not set — Telegram bot not started \
(set it in .env.local to enable)"
);
None
}
}
}
fn tailscale_deprecated_error() -> anyhow::Error {
anyhow::anyhow!(
"tm daemon --tailscale has been removed: the daemon binds loopback \
only now (ADR-0011 loopback-only doctrine, issue #3330). Use \
`trusty-console --tailscale` (or its Funnel mode) for non-loopback \
ingress instead."
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn already_running_notice_names_no_address() {
for needle in ["7880", "127.0.0.1", "http://", ":"] {
assert!(
!ALREADY_RUNNING_NOTICE.contains(needle),
"the startup guard must not print {needle:?}: {ALREADY_RUNNING_NOTICE}"
);
}
assert!(
ALREADY_RUNNING_NOTICE.contains("trusty-mpm daemon is already running"),
"the operator must still be told what happened: {ALREADY_RUNNING_NOTICE}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn bind_socket_then_publish_writes_no_lock_when_the_socket_is_taken() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("sockets").join("trusty-mpm.sock");
let lock_path = tmp.path().join("daemon.lock");
let incumbent = trusty_mpm::daemon::socket::bind(&socket)
.await
.expect("the incumbent binds a fresh path");
let err = bind_socket_then_publish("http://127.0.0.1:7880", &socket, &lock_path)
.await
.expect_err("a bind against a live socket must fail");
assert!(
format!("{err:#}").contains(&socket.display().to_string()),
"the error must name the path: {err:#}"
);
assert!(
!lock_path.exists(),
"no lock file may name a daemon that failed to start: {}",
lock_path.display()
);
drop(incumbent);
}
#[tokio::test(flavor = "multi_thread")]
async fn bind_socket_then_publish_writes_the_lock_when_the_bind_succeeds() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("sockets").join("trusty-mpm.sock");
let lock_path = tmp.path().join("daemon.lock");
let rpc = bind_socket_then_publish("http://127.0.0.1:7880", &socket, &lock_path)
.await
.expect("a fresh socket binds");
let lock = trusty_mpm::core::daemon_identity::read_lock_at(&lock_path)
.expect("the lock must be written and readable");
assert_eq!(lock.addr, "http://127.0.0.1:7880");
assert_eq!(lock.socket_path, socket.display().to_string());
assert_eq!(lock.pid, std::process::id());
drop(rpc);
}
#[tokio::test]
async fn run_daemon_rejects_tailscale_flag() {
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let err = run_daemon(addr, true, false, false, false)
.await
.expect_err("--tailscale must be rejected");
let msg = err.to_string();
assert!(msg.contains("trusty-console"), "message was: {msg}");
assert!(msg.contains("ADR-0011"), "message was: {msg}");
assert!(msg.contains("#3330"), "message was: {msg}");
}
#[test]
fn unsupervised_daemon_error_names_remedies() {
let msg = unsupervised_daemon_error().to_string();
assert!(msg.contains("launchctl kickstart"), "message was: {msg}");
assert!(msg.contains("com.trusty.mpm"), "message was: {msg}");
assert!(msg.contains("--force"), "message was: {msg}");
assert!(msg.contains("#2486"), "message was: {msg}");
assert!(msg.contains("#4397"), "message was: {msg}");
}
}