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
10#[cfg(unix)]
11use crate::core::parse_signal_name;
12use crate::core::{parse_targets, resolve_in_dir, resolve_targets_excluding_self, Process};
13use crate::error::{ProcError, Result};
14use crate::ui::{OutputFormat, Printer};
15use clap::Args;
16use dialoguer::Confirm;
17use std::path::PathBuf;
18
19/// Stop process(es) gracefully with SIGTERM
20#[derive(Args, Debug)]
21pub struct StopCommand {
22    /// Target(s): process name, PID, or :port (comma-separated for multiple)
23    #[arg(required = true)]
24    target: String,
25
26    /// Skip confirmation prompt
27    #[arg(long, short = 'y')]
28    yes: bool,
29
30    /// Show what would be stopped without actually stopping
31    #[arg(long)]
32    dry_run: bool,
33
34    /// Output as JSON
35    #[arg(long, short = 'j')]
36    json: bool,
37
38    /// Show verbose output
39    #[arg(long, short = 'v')]
40    verbose: bool,
41
42    /// Timeout in seconds to wait before force kill
43    #[arg(long, short, default_value = "10")]
44    timeout: u64,
45
46    /// Filter by directory (defaults to current directory if no path given)
47    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
48    pub in_dir: Option<String>,
49
50    /// Filter by process name
51    #[arg(long = "by", short = 'b')]
52    pub by_name: Option<String>,
53
54    /// Initial signal to send instead of SIGTERM (e.g. HUP, USR1, INT)
55    #[arg(long, short = 'S')]
56    pub signal: Option<String>,
57}
58
59impl StopCommand {
60    /// Executes the stop command, gracefully terminating matched processes.
61    pub fn execute(&self) -> Result<()> {
62        let format = if self.json {
63            OutputFormat::Json
64        } else {
65            OutputFormat::Human
66        };
67        let printer = Printer::new(format, self.verbose);
68
69        // Parse comma-separated targets and resolve to processes
70        // Use resolve_targets_excluding_self to avoid stopping ourselves
71        let targets = parse_targets(&self.target);
72        let (mut processes, not_found) = resolve_targets_excluding_self(&targets);
73
74        // Warn about targets that weren't found
75        if !not_found.is_empty() {
76            printer.warning(&format!("Not found: {}", not_found.join(", ")));
77        }
78
79        // Apply --in and --by filters
80        let in_dir_filter = resolve_in_dir(&self.in_dir);
81        processes.retain(|p| {
82            if let Some(ref dir_path) = in_dir_filter {
83                if let Some(ref cwd) = p.cwd {
84                    if !PathBuf::from(cwd).starts_with(dir_path) {
85                        return false;
86                    }
87                } else {
88                    return false;
89                }
90            }
91            if let Some(ref name) = self.by_name {
92                if !p.name.to_lowercase().contains(&name.to_lowercase()) {
93                    return false;
94                }
95            }
96            true
97        });
98
99        if processes.is_empty() {
100            return Err(ProcError::ProcessNotFound(self.target.clone()));
101        }
102
103        // Dry run: just show what would be stopped
104        if self.dry_run {
105            printer.print_processes(&processes);
106            printer.warning(&format!(
107                "Dry run: would stop {} process{}",
108                processes.len(),
109                if processes.len() == 1 { "" } else { "es" }
110            ));
111            return Ok(());
112        }
113
114        // Confirm if not --yes
115        if !self.yes && !self.json {
116            printer.print_confirmation("stop", &processes);
117
118            let prompt = format!(
119                "Stop {} process{}?",
120                processes.len(),
121                if processes.len() == 1 { "" } else { "es" }
122            );
123
124            if !Confirm::new()
125                .with_prompt(prompt)
126                .default(false)
127                .interact()?
128            {
129                printer.warning("Aborted");
130                return Ok(());
131            }
132        }
133
134        // Parse custom signal if provided
135        #[cfg(unix)]
136        let custom_signal = if let Some(ref sig_name) = self.signal {
137            Some(parse_signal_name(sig_name)?)
138        } else {
139            None
140        };
141
142        // Stop processes
143        let mut stopped = Vec::new();
144        let mut failed = Vec::new();
145
146        for proc in &processes {
147            #[cfg(unix)]
148            let send_result = if let Some(signal) = custom_signal {
149                proc.send_signal(signal)
150            } else {
151                proc.terminate()
152            };
153            #[cfg(not(unix))]
154            let send_result = proc.terminate();
155
156            match send_result {
157                Ok(()) => {
158                    // Wait for process to exit
159                    let stopped_gracefully = self.wait_for_exit(proc);
160                    if stopped_gracefully {
161                        stopped.push(proc.clone());
162                    } else {
163                        // Force kill after timeout - use kill_and_wait for reliability
164                        match proc.kill_and_wait() {
165                            Ok(_) => stopped.push(proc.clone()),
166                            Err(e) => failed.push((proc.clone(), e.to_string())),
167                        }
168                    }
169                }
170                Err(e) => failed.push((proc.clone(), e.to_string())),
171            }
172        }
173
174        // Output results
175        printer.print_action_result("Stopped", &stopped, &failed);
176
177        Ok(())
178    }
179
180    fn wait_for_exit(&self, proc: &Process) -> bool {
181        let start = std::time::Instant::now();
182        let timeout = std::time::Duration::from_secs(self.timeout);
183
184        while start.elapsed() < timeout {
185            if !proc.is_running() {
186                return true;
187            }
188            std::thread::sleep(std::time::Duration::from_millis(100));
189        }
190
191        false
192    }
193}