145 lines
5.1 KiB
Rust
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(())
|
|
}
|