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