//! 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, cmd: Option, ) -> Result<(), Box> { match cmd.unwrap_or(DaemonCmd::Run) { DaemonCmd::Run => run(config_override).await, } } async fn run(config_override: Option) -> Result<(), Box> { 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(()) }