This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/crates/maestro-daemon/src/cli.rs
T

145 lines
5.1 KiB
Rust

//! Clap-derive subcommand for the daemon, plus a runner that wires everything
//! together.
use std::error::Error;
use std::path::PathBuf;
use clap::Subcommand;
use tokio::net::TcpListener;
use crate::session::DEFAULT_LIVENESS_INTERVAL;
use crate::sidecar::SidecarStore;
use crate::tmux::Tmux;
use crate::worktree::WorktreeManager;
use crate::{
default_config_path, default_state_dir, load_config, local_api, EnvOverrides, SessionManager,
};
#[derive(Debug, Subcommand)]
pub enum DaemonCmd {
/// Start the daemon. Default when no subcommand is given.
Run,
}
/// Dispatch a parsed daemon subcommand. `config_override` is the unified
/// `--config` flag from the top-level CLI.
pub async fn dispatch(
config_override: Option<PathBuf>,
cmd: Option<DaemonCmd>,
) -> Result<(), Box<dyn Error>> {
match cmd.unwrap_or(DaemonCmd::Run) {
DaemonCmd::Run => run(config_override).await,
}
}
async fn run(config_override: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
let config_path: PathBuf = match config_override {
Some(p) => p,
None => default_config_path()?,
};
let config = load_config(&config_path, EnvOverrides::from_env())?;
tracing::info!(
hostname = %config.hostname,
bind = %config.bind_addr,
"loaded config"
);
let _ = default_state_dir();
std::fs::create_dir_all(config.sidecar_dir())?;
std::fs::create_dir_all(config.fifo_dir())?;
// First-start hook auto-install (#106). Best-effort, before serving: a
// session spawned right away should already carry the hook.
if config.hooks_auto_install {
for (target, outcome) in crate::hooks::auto_install(&config.home_dir) {
let agent = target.label();
match outcome {
crate::hooks::AutoInstallOutcome::Installed => {
tracing::info!(agent, "installed activity hook (#106)");
}
crate::hooks::AutoInstallOutcome::Upgraded => {
tracing::info!(agent, "updated activity hook to this build (#106)");
}
crate::hooks::AutoInstallOutcome::Failed => {
tracing::warn!(
agent,
"hooks auto-install failed; run `maestro hooks install` manually"
);
}
_ => {}
}
}
}
// No worktree dir to pre-create: without an override the worktrees live
// under their base repos, created on demand (#56).
let sidecar = SidecarStore::new(config.sidecar_dir());
let worktree_mgr = WorktreeManager::new(config.worktree_root.clone());
let tmux = match config.tmux_tmpdir.clone() {
Some(p) => Tmux::new().with_tmpdir(p),
None => Tmux::new(),
};
let manager = SessionManager::new_with_replay(
std::sync::Arc::new(tmux),
sidecar,
worktree_mgr,
config.fifo_dir(),
config.pty_replay_bytes,
)
.with_spawn_history(crate::spawn_history::SpawnHistory::new(
config.spawn_history_path(),
config.spawn_history_days,
));
// Spawned sessions learn where to report back (#103). Derived from the
// configured bind address; set before any spawn can happen.
manager.set_daemon_url(format!("http://{}", config.bind_addr));
let recovered = manager.recover().await?;
tracing::info!(recovered, "adopted existing tmux sessions");
manager.start_liveness_watcher(DEFAULT_LIVENESS_INTERVAL);
let projects = std::sync::Arc::new(crate::projects::ProjectScanner::new(
config.project_roots.clone(),
config.project_scan_depth,
PathBuf::from(&config.home_dir),
));
projects.start_background_refresh();
let router = local_api::router(local_api::state_for(
manager.clone(),
config.hostname.clone(),
config.home_dir.clone(),
config.spawn_presets.clone(),
projects,
config.leader.clone(),
));
let listener = TcpListener::bind(config.bind_addr).await?;
let bound = listener.local_addr()?;
tracing::info!(addr = %bound, "API listening");
// Ctrl-C at a terminal or SIGTERM from systemd: either one is a
// shutdown, and both must reach the adapter children below (#134). The
// unit's `KillMode=process` signals only the daemon, so nothing else
// would end them.
let shutdown_manager = manager.clone();
let shutdown = async move {
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("install SIGTERM handler");
tokio::select! {
_ = tokio::signal::ctrl_c() => tracing::info!("ctrl-c received — shutting down"),
_ = sigterm.recv() => tracing::info!("SIGTERM received — shutting down"),
}
// Axum waits for upgraded WebSockets to drain. Structured children
// must be settled before that wait, while the signal path still owns
// the shutdown order (#179).
shutdown_manager.shutdown().await;
};
axum::serve(listener, router)
.tcp_nodelay(true)
.with_graceful_shutdown(shutdown)
.await?;
Ok(())
}