tldr_cli/commands/daemon/
stop.rs1use std::path::PathBuf;
12
13use clap::Args;
14use serde::Serialize;
15
16use crate::output::OutputFormat;
17
18use super::error::DaemonError;
19use super::ipc::{check_socket_alive, cleanup_socket, send_command};
20use super::pid::{cleanup_stale_pid, compute_pid_path};
21use super::types::DaemonCommand;
22
23#[derive(Debug, Clone, Args)]
29pub struct DaemonStopArgs {
30 #[arg(long, short = 'p', default_value = ".")]
32 pub project: PathBuf,
33}
34
35#[derive(Debug, Clone, Serialize)]
41pub struct DaemonStopOutput {
42 pub status: String,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub message: Option<String>,
47}
48
49impl DaemonStopArgs {
54 pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
56 let runtime = tokio::runtime::Runtime::new()?;
58 runtime.block_on(self.run_async(format, quiet))
59 }
60
61 async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
63 let project = self.project.canonicalize().unwrap_or_else(|_| {
65 std::env::current_dir()
66 .unwrap_or_else(|_| PathBuf::from("."))
67 .join(&self.project)
68 });
69
70 if !check_socket_alive(&project).await {
72 let output = DaemonStopOutput {
74 status: "ok".to_string(),
75 message: Some("Daemon not running".to_string()),
76 };
77
78 if !quiet {
79 match format {
80 OutputFormat::Json | OutputFormat::Compact => {
81 println!("{}", serde_json::to_string_pretty(&output)?);
82 }
83 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
84 println!("Daemon not running");
85 }
86 }
87 }
88
89 let pid_path = compute_pid_path(&project);
91 let _ = cleanup_stale_pid(&pid_path);
92 let _ = cleanup_socket(&project);
93
94 return Ok(());
95 }
96
97 let cmd = DaemonCommand::Shutdown;
99 match send_command(&project, &cmd).await {
100 Ok(_response) => {
101 let mut retries = 0;
103 while retries < 50 {
104 if !check_socket_alive(&project).await {
106 break;
107 }
108 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
109 retries += 1;
110 }
111
112 let _ = cleanup_socket(&project);
114 let pid_path = compute_pid_path(&project);
115 let _ = cleanup_stale_pid(&pid_path);
116
117 let output = DaemonStopOutput {
118 status: "ok".to_string(),
119 message: Some("Daemon stopped".to_string()),
120 };
121
122 if !quiet {
123 match format {
124 OutputFormat::Json | OutputFormat::Compact => {
125 println!("{}", serde_json::to_string_pretty(&output)?);
126 }
127 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
128 println!("Daemon stopped");
129 }
130 }
131 }
132
133 Ok(())
134 }
135 Err(DaemonError::NotRunning) | Err(DaemonError::ConnectionRefused) => {
136 let output = DaemonStopOutput {
138 status: "ok".to_string(),
139 message: Some("Daemon not running".to_string()),
140 };
141
142 if !quiet {
143 match format {
144 OutputFormat::Json | OutputFormat::Compact => {
145 println!("{}", serde_json::to_string_pretty(&output)?);
146 }
147 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
148 println!("Daemon not running");
149 }
150 }
151 }
152
153 let _ = cleanup_socket(&project);
155 let pid_path = compute_pid_path(&project);
156 let _ = cleanup_stale_pid(&pid_path);
157
158 Ok(())
159 }
160 Err(e) => Err(anyhow::anyhow!("Failed to stop daemon: {}", e)),
161 }
162 }
163}
164
165#[cfg(test)]
170mod tests {
171 use super::*;
172 use tempfile::TempDir;
173
174 #[test]
175 fn test_daemon_stop_args_default() {
176 let args = DaemonStopArgs {
177 project: PathBuf::from("."),
178 };
179
180 assert_eq!(args.project, PathBuf::from("."));
181 }
182
183 #[test]
184 fn test_daemon_stop_output_serialization() {
185 let output = DaemonStopOutput {
186 status: "ok".to_string(),
187 message: Some("Daemon stopped".to_string()),
188 };
189
190 let json = serde_json::to_string(&output).unwrap();
191 assert!(json.contains("ok"));
192 assert!(json.contains("Daemon stopped"));
193 }
194
195 #[test]
196 fn test_daemon_stop_output_not_running() {
197 let output = DaemonStopOutput {
198 status: "ok".to_string(),
199 message: Some("Daemon not running".to_string()),
200 };
201
202 let json = serde_json::to_string(&output).unwrap();
203 assert!(json.contains("ok"));
204 assert!(json.contains("not running"));
205 }
206
207 #[tokio::test]
208 async fn test_daemon_stop_not_running() {
209 let temp = TempDir::new().unwrap();
210 let args = DaemonStopArgs {
211 project: temp.path().to_path_buf(),
212 };
213
214 let result = args.run_async(OutputFormat::Json, true).await;
216 assert!(result.is_ok());
217 }
218}