use std::net::SocketAddr;
pub(crate) async fn run_daemon(addr: SocketAddr, tailscale: bool, mcp: bool) -> anyhow::Result<()> {
use std::io::ErrorKind;
if tailscale {
return Err(tailscale_deprecated_error());
}
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();
apply_supervision_signal(&state);
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
{
eprintln!("trusty-mpm daemon is already running at {recorded_url}");
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}");
trusty_mpm::daemon::lock::write_lock(&base_url);
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 = 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).await
}
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) {
let plist_exists = crate::commands::launchd_probe::mpm_launchd_plist_exists();
let is_launchd = trusty_common::update::is_launchd_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)/com.trusty.mpm.supervisor` \
(or `com.trusty.mpm`) to let launchd own the daemon."
);
}
}
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::*;
#[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)
.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}");
}
}