tldr_cli/commands/daemon/
start.rs1use 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#[derive(Debug, Clone, Args)]
35pub struct DaemonStartArgs {
36 #[arg(long, short = 'p', default_value = ".")]
38 pub project: PathBuf,
39
40 #[arg(long)]
42 pub foreground: bool,
43}
44
45#[derive(Debug, Clone, Serialize)]
51pub struct DaemonStartOutput {
52 pub status: String,
54 pub pid: u32,
56 pub socket: PathBuf,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub message: Option<String>,
61}
62
63impl DaemonStartArgs {
68 pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
70 let runtime = tokio::runtime::Runtime::new()?;
72 runtime.block_on(self.run_async(format, quiet))
73 }
74
75 async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
77 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 let pid_path = compute_pid_path(&project);
86 if check_stale_pid(&pid_path)? {
87 cleanup_stale_pid(&pid_path)?;
88 }
89
90 let socket_path = compute_socket_path(&project);
92 if socket_path.exists() && !check_socket_alive(&project).await {
93 cleanup_socket(&project)?;
95 }
96
97 if self.foreground {
98 self.run_foreground(&project, format, quiet).await
100 } else {
101 self.run_background(&project, format, quiet).await
103 }
104 }
105
106 async fn run_foreground(
108 &self,
109 project: &Path,
110 format: OutputFormat,
111 quiet: bool,
112 ) -> anyhow::Result<()> {
113 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 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 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 let config = DaemonConfig::default();
161 let daemon = Arc::new(TLDRDaemon::new(project.to_path_buf(), config));
162 daemon.run(listener).await?;
163
164 let _ = cleanup_socket(project);
166
167 Ok(())
168 }
169
170 async fn run_background(
172 &self,
173 project: &Path,
174 format: OutputFormat,
175 quiet: bool,
176 ) -> anyhow::Result<()> {
177 if check_socket_alive(project).await {
179 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 let pid = start_daemon_background(project).await?;
191
192 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 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#[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}