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