Skip to main content

tldr_cli/commands/daemon/
start.rs

1//! Daemon start command implementation
2//!
3//! CLI command: `tldr daemon start [--project PATH] [--foreground]`
4//!
5//! This module handles starting the TLDR daemon process with:
6//! - PID file locking to ensure single instance per project
7//! - Daemonization (background mode) or foreground mode
8//! - Socket binding for IPC communication
9//!
10//! # Security Mitigations
11//!
12//! - TIGER-P1-01: Exclusive file lock on PID file prevents race conditions
13//! - TIGER-P2-02: Stale socket cleanup on startup
14
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17
18use clap::Args;
19use serde::Serialize;
20
21use crate::output::OutputFormat;
22
23use super::daemon::{start_daemon_background, wait_for_daemon, TLDRDaemon};
24use super::error::DaemonError;
25use super::ipc::{check_socket_alive, cleanup_socket, compute_socket_path, IpcListener};
26use super::pid::{check_stale_pid, cleanup_stale_pid, compute_pid_path, try_acquire_lock};
27use super::types::DaemonConfig;
28
29// =============================================================================
30// CLI Arguments
31// =============================================================================
32
33/// Arguments for the `daemon start` command.
34#[derive(Debug, Clone, Args)]
35pub struct DaemonStartArgs {
36    /// Project root directory (default: current directory)
37    #[arg(long, short = 'p', default_value = ".")]
38    pub project: PathBuf,
39
40    /// Run daemon in foreground (don't daemonize)
41    #[arg(long)]
42    pub foreground: bool,
43}
44
45// =============================================================================
46// Output Types
47// =============================================================================
48
49/// Output structure for successful daemon start.
50#[derive(Debug, Clone, Serialize)]
51pub struct DaemonStartOutput {
52    /// Status message
53    pub status: String,
54    /// PID of the daemon process
55    pub pid: u32,
56    /// Path to the socket file
57    pub socket: PathBuf,
58    /// Optional message
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub message: Option<String>,
61}
62
63// =============================================================================
64// Command Implementation
65// =============================================================================
66
67impl DaemonStartArgs {
68    /// Run the daemon start command.
69    pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
70        // Create a new tokio runtime for the async operations
71        let runtime = tokio::runtime::Runtime::new()?;
72        runtime.block_on(self.run_async(format, quiet))
73    }
74
75    /// Async implementation of the daemon start command.
76    async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
77        // Resolve project path to absolute
78        let project = self.project.canonicalize().unwrap_or_else(|_| {
79            std::env::current_dir()
80                .unwrap_or_else(|_| PathBuf::from("."))
81                .join(&self.project)
82        });
83
84        // Check for stale PID file and clean up
85        let pid_path = compute_pid_path(&project);
86        if check_stale_pid(&pid_path)? {
87            cleanup_stale_pid(&pid_path)?;
88        }
89
90        // Check for stale socket and clean up
91        let socket_path = compute_socket_path(&project);
92        if socket_path.exists() && !check_socket_alive(&project).await {
93            // Socket exists but daemon is not responding - stale
94            cleanup_socket(&project)?;
95        }
96
97        if self.foreground {
98            // Run in foreground
99            self.run_foreground(&project, format, quiet).await
100        } else {
101            // Run in background
102            self.run_background(&project, format, quiet).await
103        }
104    }
105
106    /// Run the daemon in foreground mode.
107    async fn run_foreground(
108        &self,
109        project: &Path,
110        format: OutputFormat,
111        quiet: bool,
112    ) -> anyhow::Result<()> {
113        // Try to acquire PID lock
114        let pid_path = compute_pid_path(project);
115        let _pid_guard = try_acquire_lock(&pid_path).map_err(|e| match e {
116            DaemonError::AlreadyRunning { pid } => {
117                anyhow::anyhow!("Daemon already running (PID: {})", pid)
118            }
119            DaemonError::StalePidFile { pid } => {
120                anyhow::anyhow!("Stale PID file (process {} not running)", pid)
121            }
122            other => anyhow::anyhow!("Failed to acquire lock: {}", other),
123        })?;
124
125        // Bind IPC listener
126        let listener = IpcListener::bind(project).await.map_err(|e| match e {
127            DaemonError::AddressInUse { addr } => {
128                anyhow::anyhow!("Address already in use: {}", addr)
129            }
130            DaemonError::SocketBindFailed(io_err) => {
131                anyhow::anyhow!("Failed to bind socket: {}", io_err)
132            }
133            other => anyhow::anyhow!("Socket error: {}", other),
134        })?;
135
136        let socket_path = compute_socket_path(project);
137        let our_pid = std::process::id();
138
139        // Print startup message
140        let output = DaemonStartOutput {
141            status: "ok".to_string(),
142            pid: our_pid,
143            socket: socket_path.clone(),
144            message: Some("Daemon started in foreground".to_string()),
145        };
146
147        if !quiet {
148            match format {
149                OutputFormat::Json | OutputFormat::Compact => {
150                    println!("{}", serde_json::to_string_pretty(&output)?);
151                }
152                OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
153                    println!("Daemon started with PID {}", our_pid);
154                    println!("Socket: {}", socket_path.display());
155                }
156            }
157        }
158
159        // Create and run daemon
160        let config = DaemonConfig::default();
161        let daemon = Arc::new(TLDRDaemon::new(project.to_path_buf(), config));
162        daemon.run(listener).await?;
163
164        // Cleanup socket on exit
165        let _ = cleanup_socket(project);
166
167        Ok(())
168    }
169
170    /// Run the daemon in background mode.
171    async fn run_background(
172        &self,
173        project: &Path,
174        format: OutputFormat,
175        quiet: bool,
176    ) -> anyhow::Result<()> {
177        // First check if daemon is already running
178        if check_socket_alive(project).await {
179            // Try to get PID from PID file
180            let pid_path = compute_pid_path(project);
181            let pid = std::fs::read_to_string(&pid_path)
182                .ok()
183                .and_then(|s| s.trim().parse().ok())
184                .unwrap_or(0);
185
186            return Err(anyhow::anyhow!("Daemon already running (PID: {})", pid));
187        }
188
189        // Start the daemon in background
190        let pid = start_daemon_background(project).await?;
191
192        // Wait for daemon to become ready
193        wait_for_daemon(project, 10)
194            .await
195            .map_err(|_| anyhow::anyhow!("Daemon failed to start within timeout"))?;
196
197        let socket_path = compute_socket_path(project);
198
199        // Print output
200        let output = DaemonStartOutput {
201            status: "ok".to_string(),
202            pid,
203            socket: socket_path.clone(),
204            message: Some("Daemon started".to_string()),
205        };
206
207        if !quiet {
208            match format {
209                OutputFormat::Json | OutputFormat::Compact => {
210                    println!("{}", serde_json::to_string_pretty(&output)?);
211                }
212                OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
213                    println!("Daemon started with PID {}", pid);
214                    println!("Socket: {}", socket_path.display());
215                }
216            }
217        }
218
219        Ok(())
220    }
221}
222
223// =============================================================================
224// Tests
225// =============================================================================
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    
231
232    #[test]
233    fn test_daemon_start_args_default() {
234        let args = DaemonStartArgs {
235            project: PathBuf::from("."),
236            foreground: false,
237        };
238
239        assert_eq!(args.project, PathBuf::from("."));
240        assert!(!args.foreground);
241    }
242
243    #[test]
244    fn test_daemon_start_args_foreground() {
245        let args = DaemonStartArgs {
246            project: PathBuf::from("/test/project"),
247            foreground: true,
248        };
249
250        assert!(args.foreground);
251    }
252
253    #[test]
254    fn test_daemon_start_output_serialization() {
255        let output = DaemonStartOutput {
256            status: "ok".to_string(),
257            pid: 12345,
258            socket: PathBuf::from("/tmp/tldr-abc123.sock"),
259            message: Some("Daemon started".to_string()),
260        };
261
262        let json = serde_json::to_string(&output).unwrap();
263        assert!(json.contains("ok"));
264        assert!(json.contains("12345"));
265        assert!(json.contains("tldr-abc123.sock"));
266    }
267}