tldr_cli/commands/daemon/
stop.rs1use std::path::PathBuf;
12
13use clap::Args;
14use serde::Serialize;
15
16use crate::output::OutputFormat;
17
18use super::daemon_active::remove_active;
19use super::daemon_registry::{live_entries, remove_entry};
20use super::error::DaemonError;
21use super::ipc::{check_socket_alive, cleanup_socket, send_command};
22use super::pid::{cleanup_stale_pid, compute_pid_path};
23use super::types::DaemonCommand;
24
25#[derive(Debug, Clone, Args)]
31pub struct DaemonStopArgs {
32 #[arg(long, short = 'p', default_value = ".")]
37 pub project: PathBuf,
38
39 #[arg(long, conflicts_with = "project")]
44 pub all: bool,
45}
46
47#[derive(Debug, Clone, Serialize)]
53pub struct DaemonStopOutput {
54 pub status: String,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub message: Option<String>,
59}
60
61impl DaemonStopArgs {
66 pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
68 let runtime = tokio::runtime::Runtime::new()?;
70 runtime.block_on(self.run_async(format, quiet))
71 }
72
73 async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
75 if self.all {
77 return self.run_stop_all(format, quiet).await;
78 }
79
80 let project = self.project.canonicalize().unwrap_or_else(|_| {
82 std::env::current_dir()
83 .unwrap_or_else(|_| PathBuf::from("."))
84 .join(&self.project)
85 });
86
87 if !check_socket_alive(&project).await {
89 let output = DaemonStopOutput {
91 status: "ok".to_string(),
92 message: Some("Daemon not running".to_string()),
93 };
94
95 if !quiet {
96 match format {
97 OutputFormat::Json | OutputFormat::Compact => {
98 println!("{}", serde_json::to_string_pretty(&output)?);
99 }
100 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
101 println!("Daemon not running");
102 }
103 }
104 }
105
106 let pid_path = compute_pid_path(&project);
109 let _ = cleanup_stale_pid(&pid_path);
110 let _ = cleanup_socket(&project);
111 let _ = remove_active();
112 let _ = remove_entry(&project);
113
114 return Ok(());
115 }
116
117 let cmd = DaemonCommand::Shutdown;
119 match send_command(&project, &cmd).await {
120 Ok(_response) => {
121 let mut retries = 0;
123 while retries < 50 {
124 if !check_socket_alive(&project).await {
126 break;
127 }
128 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
129 retries += 1;
130 }
131
132 let _ = cleanup_socket(&project);
134 let pid_path = compute_pid_path(&project);
135 let _ = cleanup_stale_pid(&pid_path);
136 let _ = remove_active();
137 let _ = remove_entry(&project);
138
139 let output = DaemonStopOutput {
140 status: "ok".to_string(),
141 message: Some("Daemon stopped".to_string()),
142 };
143
144 if !quiet {
145 match format {
146 OutputFormat::Json | OutputFormat::Compact => {
147 println!("{}", serde_json::to_string_pretty(&output)?);
148 }
149 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
150 println!("Daemon stopped");
151 }
152 }
153 }
154
155 Ok(())
156 }
157 Err(DaemonError::NotRunning) | Err(DaemonError::ConnectionRefused) => {
158 let output = DaemonStopOutput {
160 status: "ok".to_string(),
161 message: Some("Daemon not running".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 not running");
171 }
172 }
173 }
174
175 let _ = cleanup_socket(&project);
178 let pid_path = compute_pid_path(&project);
179 let _ = cleanup_stale_pid(&pid_path);
180 let _ = remove_active();
181 let _ = remove_entry(&project);
182
183 Ok(())
184 }
185 Err(e) => Err(anyhow::anyhow!("Failed to stop daemon: {}", e)),
186 }
187 }
188
189 async fn run_stop_all(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
195 let entries = live_entries();
196
197 if entries.is_empty() {
198 let output = DaemonStopOutput {
199 status: "ok".to_string(),
200 message: Some("No daemons running".to_string()),
201 };
202 if !quiet {
203 match format {
204 OutputFormat::Json | OutputFormat::Compact => {
205 println!("{}", serde_json::to_string_pretty(&output)?);
206 }
207 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
208 println!("No daemons running");
209 }
210 }
211 }
212 let _ = remove_active();
214 return Ok(());
215 }
216
217 let mut stopped = 0usize;
218 let mut failed = 0usize;
219 for entry in &entries {
220 let project = &entry.project;
221 let cmd = DaemonCommand::Shutdown;
222 match send_command(project, &cmd).await {
223 Ok(_response) => {
224 let mut retries = 0;
226 while retries < 50 {
227 if !check_socket_alive(project).await {
228 break;
229 }
230 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
231 retries += 1;
232 }
233 let _ = cleanup_socket(project);
234 let pid_path = compute_pid_path(project);
235 let _ = cleanup_stale_pid(&pid_path);
236 let _ = remove_entry(project);
237 stopped += 1;
238 }
239 Err(DaemonError::NotRunning) | Err(DaemonError::ConnectionRefused) => {
240 let _ = cleanup_socket(project);
242 let pid_path = compute_pid_path(project);
243 let _ = cleanup_stale_pid(&pid_path);
244 let _ = remove_entry(project);
245 stopped += 1;
246 }
247 Err(_) => {
248 failed += 1;
249 }
250 }
251 }
252 let _ = remove_active();
254
255 let summary = if failed == 0 {
256 format!("Stopped {} daemon(s)", stopped)
257 } else {
258 format!("Stopped {} daemon(s); {} failed", stopped, failed)
259 };
260 let output = DaemonStopOutput {
261 status: "ok".to_string(),
262 message: Some(summary.clone()),
263 };
264 if !quiet {
265 match format {
266 OutputFormat::Json | OutputFormat::Compact => {
267 println!("{}", serde_json::to_string_pretty(&output)?);
268 }
269 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
270 println!("{}", summary);
271 }
272 }
273 }
274 Ok(())
275 }
276}
277
278#[cfg(test)]
283mod tests {
284 use super::*;
285 use tempfile::TempDir;
286
287 #[test]
288 fn test_daemon_stop_args_default() {
289 let args = DaemonStopArgs {
290 project: PathBuf::from("."),
291 all: false,
292 };
293
294 assert_eq!(args.project, PathBuf::from("."));
295 assert!(!args.all);
296 }
297
298 #[test]
299 fn test_daemon_stop_output_serialization() {
300 let output = DaemonStopOutput {
301 status: "ok".to_string(),
302 message: Some("Daemon stopped".to_string()),
303 };
304
305 let json = serde_json::to_string(&output).unwrap();
306 assert!(json.contains("ok"));
307 assert!(json.contains("Daemon stopped"));
308 }
309
310 #[test]
311 fn test_daemon_stop_output_not_running() {
312 let output = DaemonStopOutput {
313 status: "ok".to_string(),
314 message: Some("Daemon not running".to_string()),
315 };
316
317 let json = serde_json::to_string(&output).unwrap();
318 assert!(json.contains("ok"));
319 assert!(json.contains("not running"));
320 }
321
322 #[tokio::test]
323 async fn test_daemon_stop_not_running() {
324 let temp = TempDir::new().unwrap();
325 let args = DaemonStopArgs {
326 project: temp.path().to_path_buf(),
327 all: false,
328 };
329
330 let result = args.run_async(OutputFormat::Json, true).await;
332 assert!(result.is_ok());
333 }
334}