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::daemon_active::write_active;
25use super::error::DaemonError;
26use super::ipc::{check_socket_alive, cleanup_socket, compute_socket_path, IpcListener};
27use super::pid::{compute_pid_path, try_acquire_lock};
28use super::types::DaemonConfig;
29
30#[derive(Debug, Clone, Args)]
36pub struct DaemonStartArgs {
37 #[arg(long, short = 'p', default_value = ".")]
39 pub project: PathBuf,
40
41 #[arg(long)]
43 pub foreground: bool,
44}
45
46#[derive(Debug, Clone, Serialize)]
52pub struct DaemonStartOutput {
53 pub status: String,
55 pub pid: u32,
57 pub socket: PathBuf,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub message: Option<String>,
62}
63
64impl DaemonStartArgs {
69 pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
71 let runtime = tokio::runtime::Runtime::new()?;
73 runtime.block_on(self.run_async(format, quiet))
74 }
75
76 async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
78 let project = self.project.canonicalize().unwrap_or_else(|_| {
80 std::env::current_dir()
81 .unwrap_or_else(|_| PathBuf::from("."))
82 .join(&self.project)
83 });
84
85 let socket_path = compute_socket_path(&project);
97 if socket_path.exists() && !check_socket_alive(&project).await {
98 cleanup_socket(&project)?;
100 }
101
102 if self.foreground {
103 self.run_foreground(&project, format, quiet).await
105 } else {
106 self.run_background(&project, format, quiet).await
108 }
109 }
110
111 async fn run_foreground(
113 &self,
114 project: &Path,
115 format: OutputFormat,
116 quiet: bool,
117 ) -> anyhow::Result<()> {
118 let pid_path = compute_pid_path(project);
120 let _pid_guard = try_acquire_lock(&pid_path).map_err(|e| match e {
121 DaemonError::AlreadyRunning { pid } => {
122 anyhow::anyhow!("Daemon already running (PID: {})", pid)
123 }
124 DaemonError::StalePidFile { pid } => {
125 anyhow::anyhow!("Stale PID file (process {} not running)", pid)
126 }
127 other => anyhow::anyhow!("Failed to acquire lock: {}", other),
128 })?;
129
130 let listener = IpcListener::bind(project).await.map_err(|e| match e {
132 DaemonError::AddressInUse { addr } => {
133 anyhow::anyhow!("Address already in use: {}", addr)
134 }
135 DaemonError::SocketBindFailed(io_err) => {
136 anyhow::anyhow!("Failed to bind socket: {}", io_err)
137 }
138 other => anyhow::anyhow!("Socket error: {}", other),
139 })?;
140
141 let socket_path = compute_socket_path(project);
142 let our_pid = std::process::id();
143
144 if let Err(e) = write_active(project, our_pid, &socket_path) {
150 eprintln!(
151 "warning: could not write daemon-active discovery file: {}",
152 e
153 );
154 }
155
156 let output = DaemonStartOutput {
158 status: "ok".to_string(),
159 pid: our_pid,
160 socket: socket_path.clone(),
161 message: Some("Daemon started in foreground".to_string()),
162 };
163
164 if !quiet {
165 match format {
166 OutputFormat::Json | OutputFormat::Compact => {
167 println!("{}", serde_json::to_string_pretty(&output)?);
168 }
169 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
170 println!("Daemon started with PID {}", our_pid);
171 println!("Socket: {}", socket_path.display());
172 }
173 }
174 }
175
176 let config = DaemonConfig::default();
178 let daemon = Arc::new(TLDRDaemon::new(project.to_path_buf(), config));
179 daemon.run(listener).await?;
180
181 let _ = cleanup_socket(project);
183 let _ = super::daemon_active::remove_active();
184
185 Ok(())
186 }
187
188 async fn run_background(
190 &self,
191 project: &Path,
192 format: OutputFormat,
193 quiet: bool,
194 ) -> anyhow::Result<()> {
195 if check_socket_alive(project).await {
197 let pid_path = compute_pid_path(project);
199 let pid = std::fs::read_to_string(&pid_path)
200 .ok()
201 .and_then(|s| s.trim().parse().ok())
202 .unwrap_or(0);
203
204 return Err(anyhow::anyhow!("Daemon already running (PID: {})", pid));
205 }
206
207 let pid = start_daemon_background(project).await?;
209
210 wait_for_daemon(project, 10)
212 .await
213 .map_err(|_| anyhow::anyhow!("Daemon failed to start within timeout"))?;
214
215 let socket_path = compute_socket_path(project);
216
217 let output = DaemonStartOutput {
219 status: "ok".to_string(),
220 pid,
221 socket: socket_path.clone(),
222 message: Some("Daemon started".to_string()),
223 };
224
225 if !quiet {
226 match format {
227 OutputFormat::Json | OutputFormat::Compact => {
228 println!("{}", serde_json::to_string_pretty(&output)?);
229 }
230 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
231 println!("Daemon started with PID {}", pid);
232 println!("Socket: {}", socket_path.display());
233 }
234 }
235 }
236
237 Ok(())
238 }
239}
240
241#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_daemon_start_args_default() {
251 let args = DaemonStartArgs {
252 project: PathBuf::from("."),
253 foreground: false,
254 };
255
256 assert_eq!(args.project, PathBuf::from("."));
257 assert!(!args.foreground);
258 }
259
260 #[test]
261 fn test_daemon_start_args_foreground() {
262 let args = DaemonStartArgs {
263 project: PathBuf::from("/test/project"),
264 foreground: true,
265 };
266
267 assert!(args.foreground);
268 }
269
270 #[test]
271 fn test_daemon_start_output_serialization() {
272 let output = DaemonStartOutput {
273 status: "ok".to_string(),
274 pid: 12345,
275 socket: PathBuf::from("/tmp/tldr-abc123.sock"),
276 message: Some("Daemon started".to_string()),
277 };
278
279 let json = serde_json::to_string(&output).unwrap();
280 assert!(json.contains("ok"));
281 assert!(json.contains("12345"));
282 assert!(json.contains("tldr-abc123.sock"));
283 }
284}