Skip to main content

astrid_daemon/
lib.rs

1//! Astrid Daemon — shared library for the background kernel process.
2//!
3//! This crate provides the daemon entry point as a library function so it can
4//! be reused by both the standalone `astrid-daemon` binary and the `astrid`
5//! CLI binary (which ships both via `cargo install astrid`).
6
7#![deny(unsafe_code)]
8#![deny(missing_docs)]
9#![deny(clippy::all)]
10#![deny(unreachable_pub)]
11#![deny(clippy::unwrap_used)]
12
13use anyhow::{Context, Result};
14use clap::Parser;
15
16/// Astrid Daemon - Background kernel process
17#[derive(Parser)]
18#[command(name = "astrid-daemon")]
19#[command(author, version, about)]
20pub struct Args {
21    /// The session ID to bind the daemon to
22    #[arg(short, long, default_value = "00000000-0000-0000-0000-000000000000")]
23    pub session: String,
24
25    /// Workspace root directory
26    #[arg(short, long)]
27    pub workspace: Option<std::path::PathBuf>,
28
29    /// Enable ephemeral mode (auto-shutdown on idle timeout after last client disconnects)
30    #[arg(long)]
31    pub ephemeral: bool,
32
33    /// Enable verbose logging
34    #[arg(short, long)]
35    pub verbose: bool,
36}
37
38fn init_logging(verbose: bool) {
39    let workspace_root = std::env::current_dir().ok();
40    let unified_cfg = astrid_config::Config::load(workspace_root.as_deref())
41        .ok()
42        .map(|r| r.config);
43
44    let log_config = if let Some(cfg) = &unified_cfg {
45        let mut lc = astrid_telemetry::log_config_from(cfg);
46        if verbose {
47            "debug".clone_into(&mut lc.level);
48        }
49        if let Ok(home) = astrid_core::dirs::AstridHome::resolve() {
50            lc.target = astrid_telemetry::LogTarget::File(home.log_dir());
51        }
52        lc
53    } else {
54        let level = if verbose { "debug" } else { "info" };
55        let mut lc = astrid_telemetry::LogConfig::new(level)
56            .with_format(astrid_telemetry::LogFormat::Compact);
57        if let Ok(home) = astrid_core::dirs::AstridHome::resolve() {
58            lc.target = astrid_telemetry::LogTarget::File(home.log_dir());
59        }
60        lc
61    };
62
63    if let Err(e) = astrid_telemetry::setup_logging(&log_config) {
64        eprintln!("Failed to initialize logging: {e}");
65    }
66}
67
68/// Run the Astrid daemon with the given arguments.
69///
70/// This is the shared entry point used by both the standalone `astrid-daemon`
71/// binary and the `astrid` CLI's bundled daemon binary.
72///
73/// # Errors
74///
75/// Returns an error if the kernel fails to boot, the CLI proxy capsule is
76/// missing, or the readiness file cannot be written.
77pub async fn run() -> Result<()> {
78    let args = Args::parse();
79
80    init_logging(args.verbose);
81
82    let session_id = astrid_core::SessionId::from_uuid(
83        uuid::Uuid::parse_str(&args.session)
84            .map_err(|e| anyhow::anyhow!("Invalid UUID format: {e}"))?,
85    );
86
87    let ws = args.workspace.unwrap_or_else(|| {
88        std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
89    });
90
91    let kernel = astrid_kernel::Kernel::new(session_id.clone(), ws)
92        .await
93        .map_err(|e| anyhow::anyhow!("Failed to boot Kernel: {e}"))?;
94
95    // In ephemeral mode, shut down immediately when the last client disconnects.
96    if args.ephemeral {
97        kernel.set_ephemeral(true);
98    }
99
100    // Load all capsules (auto-discovery)
101    kernel.load_all_capsules().await;
102
103    // Verify the CLI proxy capsule loaded. Without it, the daemon
104    // has no accept loop and CLI connections will always time out.
105    {
106        let reg = kernel.capsules.read().await;
107        let has_cli_proxy = reg
108            .list()
109            .iter()
110            .any(|id| id.as_str() == "astrid-capsule-cli");
111        if !has_cli_proxy {
112            tracing::error!(
113                "CLI proxy capsule (astrid-capsule-cli) not found - \
114                 daemon cannot accept CLI connections"
115            );
116            anyhow::bail!(
117                "CLI proxy capsule (astrid-capsule-cli) not found. \
118                 Install it with: astrid capsule install @unicity-astrid/capsule-cli"
119            );
120        }
121    }
122
123    // Signal readiness AFTER all capsules are loaded and accepting
124    // connections. The CLI polls for this file to avoid connecting
125    // before the handshake accept loop is running.
126    astrid_kernel::socket::write_readiness_file().map_err(|e| {
127        anyhow::anyhow!(
128            "Failed to write readiness file \
129             (daemon is useless without it): {e}"
130        )
131    })?;
132
133    tracing::info!(
134        session = %session_id.0,
135        ephemeral = args.ephemeral,
136        "Kernel booted successfully"
137    );
138
139    // Wait for a termination signal or API shutdown request.
140    let mut shutdown_rx = kernel.shutdown_tx.subscribe();
141
142    #[cfg(unix)]
143    {
144        let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
145            .context("failed to register SIGTERM handler")?;
146        tokio::select! {
147            _ = tokio::signal::ctrl_c() => {
148                tracing::info!("Received SIGINT, shutting down");
149            }
150            _ = sigterm.recv() => {
151                tracing::info!("Received SIGTERM, shutting down");
152            }
153            _ = shutdown_rx.wait_for(|v| *v) => {
154                tracing::info!("Received API shutdown request, shutting down");
155            }
156        }
157    }
158    #[cfg(not(unix))]
159    {
160        tokio::select! {
161            _ = tokio::signal::ctrl_c() => {
162                tracing::info!("Received SIGINT, shutting down");
163            }
164            _ = shutdown_rx.wait_for(|v| *v) => {
165                tracing::info!("Received API shutdown request, shutting down");
166            }
167        }
168    }
169
170    kernel.shutdown(Some("signal".to_string())).await;
171
172    Ok(())
173}