Skip to main content

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