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::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// =============================================================================
24// CLI Arguments
25// =============================================================================
26
27/// Arguments for the `daemon stop` command.
28#[derive(Debug, Clone, Args)]
29pub struct DaemonStopArgs {
30    /// Project root directory (default: current directory)
31    #[arg(long, short = 'p', default_value = ".")]
32    pub project: PathBuf,
33}
34
35// =============================================================================
36// Output Types
37// =============================================================================
38
39/// Output structure for daemon stop result.
40#[derive(Debug, Clone, Serialize)]
41pub struct DaemonStopOutput {
42    /// Status message
43    pub status: String,
44    /// Optional message
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub message: Option<String>,
47}
48
49// =============================================================================
50// Command Implementation
51// =============================================================================
52
53impl DaemonStopArgs {
54    /// Run the daemon stop command.
55    pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
56        // Create a new tokio runtime for the async operations
57        let runtime = tokio::runtime::Runtime::new()?;
58        runtime.block_on(self.run_async(format, quiet))
59    }
60
61    /// Async implementation of the daemon stop command.
62    async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
63        // Resolve project path to absolute
64        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        // Check if daemon is running
71        if !check_socket_alive(&project).await {
72            // Daemon not running
73            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            // Clean up any stale files
90            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        // Send shutdown command
98        let cmd = DaemonCommand::Shutdown;
99        match send_command(&project, &cmd).await {
100            Ok(_response) => {
101                // Wait for daemon to actually stop
102                let mut retries = 0;
103                while retries < 50 {
104                    // 5 seconds max
105                    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                // Clean up files
113                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                // Daemon already stopped or not responding
137                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                // Clean up any stale files
154                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// =============================================================================
166// Tests
167// =============================================================================
168
169#[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        // Should succeed when daemon is not running
215        let result = args.run_async(OutputFormat::Json, true).await;
216        assert!(result.is_ok());
217    }
218}