Skip to main content

tldr_cli/commands/daemon/
stop.rs

1//! Daemon stop command implementation
2//!
3//! CLI command: `tldr daemon stop [--project PATH]`
4//!
5//! This module handles stopping the TLDR daemon gracefully by:
6//! - Connecting to the daemon via IPC
7//! - Sending a shutdown command
8//! - Waiting for the daemon to exit
9//! - Cleaning up socket and PID files
10
11use std::path::PathBuf;
12
13use clap::Args;
14use serde::Serialize;
15
16use crate::output::OutputFormat;
17
18use super::daemon_active::remove_active;
19use super::error::DaemonError;
20use super::ipc::{check_socket_alive, cleanup_socket, send_command};
21use super::pid::{cleanup_stale_pid, compute_pid_path};
22use super::types::DaemonCommand;
23
24// =============================================================================
25// CLI Arguments
26// =============================================================================
27
28/// Arguments for the `daemon stop` command.
29#[derive(Debug, Clone, Args)]
30pub struct DaemonStopArgs {
31    /// Project root directory (default: current directory)
32    #[arg(long, short = 'p', default_value = ".")]
33    pub project: PathBuf,
34}
35
36// =============================================================================
37// Output Types
38// =============================================================================
39
40/// Output structure for daemon stop result.
41#[derive(Debug, Clone, Serialize)]
42pub struct DaemonStopOutput {
43    /// Status message
44    pub status: String,
45    /// Optional message
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub message: Option<String>,
48}
49
50// =============================================================================
51// Command Implementation
52// =============================================================================
53
54impl DaemonStopArgs {
55    /// Run the daemon stop command.
56    pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
57        // Create a new tokio runtime for the async operations
58        let runtime = tokio::runtime::Runtime::new()?;
59        runtime.block_on(self.run_async(format, quiet))
60    }
61
62    /// Async implementation of the daemon stop command.
63    async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
64        // Resolve project path to absolute
65        let project = self.project.canonicalize().unwrap_or_else(|_| {
66            std::env::current_dir()
67                .unwrap_or_else(|_| PathBuf::from("."))
68                .join(&self.project)
69        });
70
71        // Check if daemon is running
72        if !check_socket_alive(&project).await {
73            // Daemon not running
74            let output = DaemonStopOutput {
75                status: "ok".to_string(),
76                message: Some("Daemon not running".to_string()),
77            };
78
79            if !quiet {
80                match format {
81                    OutputFormat::Json | OutputFormat::Compact => {
82                        println!("{}", serde_json::to_string_pretty(&output)?);
83                    }
84                    OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
85                        println!("Daemon not running");
86                    }
87                }
88            }
89
90            // Clean up any stale files (incl. VAL-013 active-daemon record).
91            let pid_path = compute_pid_path(&project);
92            let _ = cleanup_stale_pid(&pid_path);
93            let _ = cleanup_socket(&project);
94            let _ = remove_active();
95
96            return Ok(());
97        }
98
99        // Send shutdown command
100        let cmd = DaemonCommand::Shutdown;
101        match send_command(&project, &cmd).await {
102            Ok(_response) => {
103                // Wait for daemon to actually stop
104                let mut retries = 0;
105                while retries < 50 {
106                    // 5 seconds max
107                    if !check_socket_alive(&project).await {
108                        break;
109                    }
110                    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
111                    retries += 1;
112                }
113
114                // Clean up files (incl. VAL-013 active-daemon record).
115                let _ = cleanup_socket(&project);
116                let pid_path = compute_pid_path(&project);
117                let _ = cleanup_stale_pid(&pid_path);
118                let _ = remove_active();
119
120                let output = DaemonStopOutput {
121                    status: "ok".to_string(),
122                    message: Some("Daemon stopped".to_string()),
123                };
124
125                if !quiet {
126                    match format {
127                        OutputFormat::Json | OutputFormat::Compact => {
128                            println!("{}", serde_json::to_string_pretty(&output)?);
129                        }
130                        OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
131                            println!("Daemon stopped");
132                        }
133                    }
134                }
135
136                Ok(())
137            }
138            Err(DaemonError::NotRunning) | Err(DaemonError::ConnectionRefused) => {
139                // Daemon already stopped or not responding
140                let output = DaemonStopOutput {
141                    status: "ok".to_string(),
142                    message: Some("Daemon not running".to_string()),
143                };
144
145                if !quiet {
146                    match format {
147                        OutputFormat::Json | OutputFormat::Compact => {
148                            println!("{}", serde_json::to_string_pretty(&output)?);
149                        }
150                        OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
151                            println!("Daemon not running");
152                        }
153                    }
154                }
155
156                // Clean up any stale files (incl. VAL-013 active-daemon record).
157                let _ = cleanup_socket(&project);
158                let pid_path = compute_pid_path(&project);
159                let _ = cleanup_stale_pid(&pid_path);
160                let _ = remove_active();
161
162                Ok(())
163            }
164            Err(e) => Err(anyhow::anyhow!("Failed to stop daemon: {}", e)),
165        }
166    }
167}
168
169// =============================================================================
170// Tests
171// =============================================================================
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use tempfile::TempDir;
177
178    #[test]
179    fn test_daemon_stop_args_default() {
180        let args = DaemonStopArgs {
181            project: PathBuf::from("."),
182        };
183
184        assert_eq!(args.project, PathBuf::from("."));
185    }
186
187    #[test]
188    fn test_daemon_stop_output_serialization() {
189        let output = DaemonStopOutput {
190            status: "ok".to_string(),
191            message: Some("Daemon stopped".to_string()),
192        };
193
194        let json = serde_json::to_string(&output).unwrap();
195        assert!(json.contains("ok"));
196        assert!(json.contains("Daemon stopped"));
197    }
198
199    #[test]
200    fn test_daemon_stop_output_not_running() {
201        let output = DaemonStopOutput {
202            status: "ok".to_string(),
203            message: Some("Daemon not running".to_string()),
204        };
205
206        let json = serde_json::to_string(&output).unwrap();
207        assert!(json.contains("ok"));
208        assert!(json.contains("not running"));
209    }
210
211    #[tokio::test]
212    async fn test_daemon_stop_not_running() {
213        let temp = TempDir::new().unwrap();
214        let args = DaemonStopArgs {
215            project: temp.path().to_path_buf(),
216        };
217
218        // Should succeed when daemon is not running
219        let result = args.run_async(OutputFormat::Json, true).await;
220        assert!(result.is_ok());
221    }
222}