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_excluding_self, Process};
11use crate::error::{ProcError, Result};
12use crate::ui::{OutputFormat, Printer};
13use clap::Args;
14use dialoguer::Confirm;
15use serde::Serialize;
16use std::path::PathBuf;
17
18/// Stop process(es) gracefully with SIGTERM
19#[derive(Args, Debug)]
20pub struct StopCommand {
21    /// Target(s): process name, PID, or :port (comma-separated for multiple)
22    #[arg(required = true)]
23    target: String,
24
25    /// Skip confirmation prompt
26    #[arg(long, short = 'y')]
27    yes: bool,
28
29    /// Show what would be stopped without actually stopping
30    #[arg(long)]
31    dry_run: bool,
32
33    /// Output as JSON
34    #[arg(long, short)]
35    json: bool,
36
37    /// Timeout in seconds to wait before force kill
38    #[arg(long, short, default_value = "10")]
39    timeout: u64,
40
41    /// Filter by directory (defaults to current directory if no path given)
42    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
43    pub in_dir: Option<String>,
44
45    /// Filter by process name
46    #[arg(long = "by", short = 'b')]
47    pub by_name: Option<String>,
48}
49
50impl StopCommand {
51    /// Executes the stop command, gracefully terminating matched processes.
52    pub fn execute(&self) -> Result<()> {
53        let format = if self.json {
54            OutputFormat::Json
55        } else {
56            OutputFormat::Human
57        };
58        let printer = Printer::new(format, false);
59
60        // Parse comma-separated targets and resolve to processes
61        // Use resolve_targets_excluding_self to avoid stopping ourselves
62        let targets = parse_targets(&self.target);
63        let (mut processes, not_found) = resolve_targets_excluding_self(&targets);
64
65        // Warn about targets that weren't found
66        for target in &not_found {
67            printer.warning(&format!("Target not found: {}", target));
68        }
69
70        // Apply --in and --by filters
71        let in_dir_filter = resolve_in_dir(&self.in_dir);
72        processes.retain(|p| {
73            if let Some(ref dir_path) = in_dir_filter {
74                if let Some(ref cwd) = p.cwd {
75                    if !PathBuf::from(cwd).starts_with(dir_path) {
76                        return false;
77                    }
78                } else {
79                    return false;
80                }
81            }
82            if let Some(ref name) = self.by_name {
83                if !p.name.to_lowercase().contains(&name.to_lowercase()) {
84                    return false;
85                }
86            }
87            true
88        });
89
90        if processes.is_empty() {
91            return Err(ProcError::ProcessNotFound(self.target.clone()));
92        }
93
94        // Dry run: just show what would be stopped
95        if self.dry_run {
96            printer.print_processes(&processes);
97            printer.warning(&format!(
98                "Dry run: would stop {} process{}",
99                processes.len(),
100                if processes.len() == 1 { "" } else { "es" }
101            ));
102            return Ok(());
103        }
104
105        // Confirm if not --yes
106        if !self.yes && !self.json {
107            self.show_processes(&processes);
108
109            let prompt = format!(
110                "Stop {} process{}?",
111                processes.len(),
112                if processes.len() == 1 { "" } else { "es" }
113            );
114
115            if !Confirm::new()
116                .with_prompt(prompt)
117                .default(false)
118                .interact()?
119            {
120                printer.warning("Aborted");
121                return Ok(());
122            }
123        }
124
125        // Stop processes
126        let mut stopped = Vec::new();
127        let mut failed = Vec::new();
128
129        for proc in &processes {
130            match proc.terminate() {
131                Ok(()) => {
132                    // Wait for process to exit
133                    let stopped_gracefully = self.wait_for_exit(proc);
134                    if stopped_gracefully {
135                        stopped.push(proc.clone());
136                    } else {
137                        // Force kill after timeout - use kill_and_wait for reliability
138                        match proc.kill_and_wait() {
139                            Ok(_) => stopped.push(proc.clone()),
140                            Err(e) => failed.push((proc.clone(), e.to_string())),
141                        }
142                    }
143                }
144                Err(e) => failed.push((proc.clone(), e.to_string())),
145            }
146        }
147
148        // Output results
149        if self.json {
150            printer.print_json(&StopOutput {
151                action: "stop",
152                success: failed.is_empty(),
153                stopped_count: stopped.len(),
154                failed_count: failed.len(),
155                stopped: &stopped,
156                failed: &failed
157                    .iter()
158                    .map(|(p, e)| FailedStop {
159                        process: p,
160                        error: e,
161                    })
162                    .collect::<Vec<_>>(),
163            });
164        } else {
165            self.print_results(&printer, &stopped, &failed);
166        }
167
168        Ok(())
169    }
170
171    fn wait_for_exit(&self, proc: &Process) -> bool {
172        let start = std::time::Instant::now();
173        let timeout = std::time::Duration::from_secs(self.timeout);
174
175        while start.elapsed() < timeout {
176            if !proc.is_running() {
177                return true;
178            }
179            std::thread::sleep(std::time::Duration::from_millis(100));
180        }
181
182        false
183    }
184
185    fn show_processes(&self, processes: &[Process]) {
186        use colored::*;
187
188        println!(
189            "\n{} Found {} process{}:\n",
190            "!".yellow().bold(),
191            processes.len().to_string().cyan().bold(),
192            if processes.len() == 1 { "" } else { "es" }
193        );
194
195        for proc in processes {
196            println!(
197                "  {} {} [PID {}] - {:.1}% CPU, {:.1} MB",
198                "→".bright_black(),
199                proc.name.white().bold(),
200                proc.pid.to_string().cyan(),
201                proc.cpu_percent,
202                proc.memory_mb
203            );
204        }
205        println!();
206    }
207
208    fn print_results(&self, printer: &Printer, stopped: &[Process], failed: &[(Process, String)]) {
209        use colored::*;
210
211        if !stopped.is_empty() {
212            println!(
213                "{} Stopped {} process{}",
214                "✓".green().bold(),
215                stopped.len().to_string().cyan().bold(),
216                if stopped.len() == 1 { "" } else { "es" }
217            );
218            for proc in stopped {
219                println!(
220                    "  {} {} [PID {}]",
221                    "→".bright_black(),
222                    proc.name.white(),
223                    proc.pid.to_string().cyan()
224                );
225            }
226        }
227
228        if !failed.is_empty() {
229            printer.error(&format!(
230                "Failed to stop {} process{}",
231                failed.len(),
232                if failed.len() == 1 { "" } else { "es" }
233            ));
234            for (proc, err) in failed {
235                println!(
236                    "  {} {} [PID {}]: {}",
237                    "→".bright_black(),
238                    proc.name.white(),
239                    proc.pid.to_string().cyan(),
240                    err.red()
241                );
242            }
243        }
244    }
245}
246
247#[derive(Serialize)]
248struct StopOutput<'a> {
249    action: &'static str,
250    success: bool,
251    stopped_count: usize,
252    failed_count: usize,
253    stopped: &'a [Process],
254    failed: &'a [FailedStop<'a>],
255}
256
257#[derive(Serialize)]
258struct FailedStop<'a> {
259    process: &'a Process,
260    error: &'a str,
261}
262
263fn resolve_in_dir(in_dir: &Option<String>) -> Option<PathBuf> {
264    in_dir.as_ref().map(|p| {
265        if p == "." {
266            std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
267        } else {
268            let path = PathBuf::from(p);
269            if path.is_relative() {
270                std::env::current_dir()
271                    .unwrap_or_else(|_| PathBuf::from("."))
272                    .join(path)
273            } else {
274                path
275            }
276        }
277    })
278}