Skip to main content

cortex_runtime/cli/
start.rs

1//! Start the Cortex daemon process.
2
3use crate::cartography::mapper::Mapper;
4use crate::cli::output::{self, Styled};
5use crate::extraction::loader::ExtractionLoader;
6use crate::renderer::chromium::ChromiumRenderer;
7use crate::renderer::{NoopRenderer, Renderer};
8use crate::server::Server;
9use anyhow::{Context, Result};
10use std::path::PathBuf;
11use std::sync::Arc;
12use tracing::{error, info, warn};
13
14/// Default socket path.
15pub const SOCKET_PATH: &str = "/tmp/cortex.sock";
16
17/// Get the PID file path.
18pub fn pid_file_path() -> PathBuf {
19    dirs::home_dir()
20        .unwrap_or_else(|| PathBuf::from("/tmp"))
21        .join(".cortex/cortex.pid")
22}
23
24/// Check if Cortex is already running. Returns the PID if so.
25pub fn check_already_running() -> Option<i32> {
26    let pid_path = pid_file_path();
27    if !pid_path.exists() {
28        return None;
29    }
30    let pid_str = std::fs::read_to_string(&pid_path).ok()?;
31    let pid: i32 = pid_str.trim().parse().ok()?;
32
33    // Check if the process is actually alive
34    #[cfg(unix)]
35    {
36        let output = std::process::Command::new("kill")
37            .args(["-0", &pid.to_string()])
38            .output();
39        if matches!(output, Ok(o) if o.status.success()) {
40            return Some(pid);
41        }
42    }
43
44    // Stale PID file — clean up
45    let _ = std::fs::remove_file(&pid_path);
46    None
47}
48
49/// Start the Cortex daemon with optional REST API.
50pub async fn run_with_http(http_port: Option<u16>) -> Result<()> {
51    run_inner(http_port).await
52}
53
54/// Start the Cortex daemon: bind socket, write PID, serve requests.
55pub async fn run() -> Result<()> {
56    run_inner(None).await
57}
58
59async fn run_inner(http_port: Option<u16>) -> Result<()> {
60    let s = Styled::new();
61
62    // Check if already running
63    if let Some(pid) = check_already_running() {
64        eprintln!("  {} Cortex is already running (PID {pid}).", s.warn_sym());
65        eprintln!("  Use 'cortex restart' or 'cortex stop' first.");
66        std::process::exit(1);
67    }
68
69    // Clean up stale socket file
70    let socket_path = PathBuf::from(SOCKET_PATH);
71    if socket_path.exists() {
72        std::fs::remove_file(&socket_path).ok();
73    }
74
75    // Ensure ~/.cortex/ exists
76    let pid_path = pid_file_path();
77    if let Some(parent) = pid_path.parent() {
78        std::fs::create_dir_all(parent).ok();
79    }
80
81    // Initialize tracing
82    tracing_subscriber::fmt()
83        .with_env_filter(
84            tracing_subscriber::EnvFilter::from_default_env()
85                .add_directive("cortex=info".parse().unwrap()),
86        )
87        .init();
88
89    info!("starting Cortex v{}", env!("CARGO_PKG_VERSION"));
90
91    // Write PID file
92    std::fs::write(&pid_path, std::process::id().to_string())
93        .context("failed to write PID file")?;
94
95    if !output::is_quiet() {
96        eprintln!(
97            "  {} Cortex v{} started (PID {})",
98            s.ok_sym(),
99            env!("CARGO_PKG_VERSION"),
100            std::process::id()
101        );
102        eprintln!("  Listening on {SOCKET_PATH}");
103    }
104
105    // Initialize browser renderer
106    let server = match ChromiumRenderer::new().await {
107        Ok(renderer) => {
108            info!("Chromium renderer initialized");
109            let renderer: Arc<dyn Renderer> = Arc::new(renderer);
110
111            // Initialize extraction loader
112            let extractor_loader = match ExtractionLoader::new() {
113                Ok(loader) => {
114                    info!("Extraction loader initialized");
115                    Arc::new(loader)
116                }
117                Err(e) => {
118                    warn!("Failed to initialize extraction loader: {e}");
119                    warn!("MAP requests will use fallback extractors");
120                    Arc::new(
121                        ExtractionLoader::new()
122                            .unwrap_or_else(|_| panic!("ExtractionLoader must initialize")),
123                    )
124                }
125            };
126
127            // Create mapper
128            let mapper = Arc::new(Mapper::new(Arc::clone(&renderer), extractor_loader));
129
130            Server::new(&socket_path).with_mapper(renderer, mapper)
131        }
132        Err(e) => {
133            warn!("Failed to initialize Chromium: {e}");
134            warn!("Running in HTTP-only mode (no browser fallback, no PERCEIVE)");
135            let renderer: Arc<dyn Renderer> = Arc::new(NoopRenderer);
136            let extractor_loader = Arc::new(
137                ExtractionLoader::new()
138                    .unwrap_or_else(|_| panic!("ExtractionLoader must initialize")),
139            );
140            let mapper = Arc::new(Mapper::new(Arc::clone(&renderer), extractor_loader));
141            Server::new(&socket_path).with_mapper(renderer, mapper)
142        }
143    };
144
145    let shutdown = server.shutdown_handle();
146
147    // Set up SIGTERM/SIGINT handling
148    let shutdown_signal = shutdown.clone();
149    tokio::spawn(async move {
150        let _ = tokio::signal::ctrl_c().await;
151        info!("received shutdown signal");
152        shutdown_signal.notify_one();
153    });
154
155    // Optionally start REST API
156    if let Some(port) = http_port {
157        let rest_state = server.shared_state();
158        tokio::spawn(async move {
159            if let Err(e) = crate::rest::start(port, rest_state).await {
160                error!("REST API error: {e}");
161            }
162        });
163        if !output::is_quiet() {
164            eprintln!("  REST API listening on http://127.0.0.1:{port}");
165        }
166    }
167
168    // Run server
169    let result = server.start().await;
170
171    // Clean up on exit
172    let _ = std::fs::remove_file(&pid_path);
173    let _ = std::fs::remove_file(&socket_path);
174
175    if !output::is_quiet() {
176        eprintln!("  {} Cortex stopped.", s.ok_sym());
177    }
178
179    result
180}