proc_cli/commands/
stop.rs

1//! Stop command - Graceful process termination (SIGTERM)
2//!
3//! Usage:
4//!   proc stop 1234          # Stop PID 1234
5//!   proc stop :3000         # Stop process on port 3000
6//!   proc stop node          # Stop all node processes
7
8use crate::core::{resolve_target, Process};
9use crate::error::{ProcError, Result};
10use crate::ui::{OutputFormat, Printer};
11use clap::Args;
12use dialoguer::Confirm;
13use serde::Serialize;
14
15/// Stop process(es) gracefully with SIGTERM
16#[derive(Args, Debug)]
17pub struct StopCommand {
18    /// Target: process name, PID, or :port
19    #[arg(required = true)]
20    target: String,
21
22    /// Skip confirmation prompt
23    #[arg(long, short = 'y')]
24    yes: bool,
25
26    /// Output as JSON
27    #[arg(long, short)]
28    json: bool,
29
30    /// Timeout in seconds to wait before force kill
31    #[arg(long, short, default_value = "10")]
32    timeout: u64,
33}
34
35impl StopCommand {
36    /// Executes the stop command, gracefully terminating matched processes.
37    pub fn execute(&self) -> Result<()> {
38        let format = if self.json {
39            OutputFormat::Json
40        } else {
41            OutputFormat::Human
42        };
43        let printer = Printer::new(format, false);
44
45        // Parse target
46        let processes = self.find_target_processes()?;
47
48        if processes.is_empty() {
49            return Err(ProcError::ProcessNotFound(self.target.clone()));
50        }
51
52        // Confirm if not --yes
53        if !self.yes && !self.json {
54            self.show_processes(&processes);
55
56            let prompt = format!(
57                "Stop {} process{}?",
58                processes.len(),
59                if processes.len() == 1 { "" } else { "es" }
60            );
61
62            if !Confirm::new()
63                .with_prompt(prompt)
64                .default(false)
65                .interact()?
66            {
67                printer.warning("Aborted");
68                return Ok(());
69            }
70        }
71
72        // Stop processes
73        let mut stopped = Vec::new();
74        let mut failed = Vec::new();
75
76        for proc in &processes {
77            match proc.terminate() {
78                Ok(()) => {
79                    // Wait for process to exit
80                    let stopped_gracefully = self.wait_for_exit(proc);
81                    if stopped_gracefully {
82                        stopped.push(proc.clone());
83                    } else {
84                        // Force kill after timeout - use kill_and_wait for reliability
85                        match proc.kill_and_wait() {
86                            Ok(_) => stopped.push(proc.clone()),
87                            Err(e) => failed.push((proc.clone(), e.to_string())),
88                        }
89                    }
90                }
91                Err(e) => failed.push((proc.clone(), e.to_string())),
92            }
93        }
94
95        // Output results
96        if self.json {
97            printer.print_json(&StopOutput {
98                action: "stop",
99                success: failed.is_empty(),
100                stopped_count: stopped.len(),
101                failed_count: failed.len(),
102                stopped: &stopped,
103                failed: &failed
104                    .iter()
105                    .map(|(p, e)| FailedStop {
106                        process: p,
107                        error: e,
108                    })
109                    .collect::<Vec<_>>(),
110            });
111        } else {
112            self.print_results(&printer, &stopped, &failed);
113        }
114
115        Ok(())
116    }
117
118    fn find_target_processes(&self) -> Result<Vec<Process>> {
119        resolve_target(&self.target)
120    }
121
122    fn wait_for_exit(&self, proc: &Process) -> bool {
123        let start = std::time::Instant::now();
124        let timeout = std::time::Duration::from_secs(self.timeout);
125
126        while start.elapsed() < timeout {
127            if !proc.is_running() {
128                return true;
129            }
130            std::thread::sleep(std::time::Duration::from_millis(100));
131        }
132
133        false
134    }
135
136    fn show_processes(&self, processes: &[Process]) {
137        use colored::*;
138
139        println!(
140            "\n{} Found {} process{}:\n",
141            "!".yellow().bold(),
142            processes.len().to_string().cyan().bold(),
143            if processes.len() == 1 { "" } else { "es" }
144        );
145
146        for proc in processes {
147            println!(
148                "  {} {} [PID {}] - {:.1}% CPU, {:.1} MB",
149                "→".bright_black(),
150                proc.name.white().bold(),
151                proc.pid.to_string().cyan(),
152                proc.cpu_percent,
153                proc.memory_mb
154            );
155        }
156        println!();
157    }
158
159    fn print_results(&self, printer: &Printer, stopped: &[Process], failed: &[(Process, String)]) {
160        use colored::*;
161
162        if !stopped.is_empty() {
163            println!(
164                "{} Stopped {} process{}",
165                "✓".green().bold(),
166                stopped.len().to_string().cyan().bold(),
167                if stopped.len() == 1 { "" } else { "es" }
168            );
169            for proc in stopped {
170                println!(
171                    "  {} {} [PID {}]",
172                    "→".bright_black(),
173                    proc.name.white(),
174                    proc.pid.to_string().cyan()
175                );
176            }
177        }
178
179        if !failed.is_empty() {
180            printer.error(&format!(
181                "Failed to stop {} process{}",
182                failed.len(),
183                if failed.len() == 1 { "" } else { "es" }
184            ));
185            for (proc, err) in failed {
186                println!(
187                    "  {} {} [PID {}]: {}",
188                    "→".bright_black(),
189                    proc.name.white(),
190                    proc.pid.to_string().cyan(),
191                    err.red()
192                );
193            }
194        }
195    }
196}
197
198#[derive(Serialize)]
199struct StopOutput<'a> {
200    action: &'static str,
201    success: bool,
202    stopped_count: usize,
203    failed_count: usize,
204    stopped: &'a [Process],
205    failed: &'a [FailedStop<'a>],
206}
207
208#[derive(Serialize)]
209struct FailedStop<'a> {
210    process: &'a Process,
211    error: &'a str,
212}