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::{apply_filters, parse_targets, resolve_targets_excluding_self, Process};
13use crate::error::{ProcError, Result};
14use crate::ui::Printer;
15use clap::Args;
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 = 'j')]
34    json: bool,
35
36    /// Show verbose output
37    #[arg(long, short = 'v')]
38    verbose: bool,
39
40    /// Timeout in seconds to wait before force kill
41    #[arg(long, short, default_value = "10")]
42    timeout: u64,
43
44    /// Filter by directory (defaults to current directory if no path given)
45    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
46    pub in_dir: Option<String>,
47
48    /// Filter by process name
49    #[arg(long = "by", short = 'b')]
50    pub by_name: Option<String>,
51
52    /// Initial signal to send instead of SIGTERM (e.g. HUP, USR1, INT)
53    #[arg(long, short = 'S')]
54    pub signal: Option<String>,
55}
56
57impl StopCommand {
58    /// Executes the stop command, gracefully terminating matched processes.
59    pub fn execute(&self) -> Result<()> {
60        let printer = Printer::from_flags(self.json, self.verbose);
61
62        // Parse comma-separated targets and resolve to processes
63        // Use resolve_targets_excluding_self to avoid stopping ourselves
64        let targets = parse_targets(&self.target);
65        let (mut processes, not_found) = resolve_targets_excluding_self(&targets);
66
67        // Warn about targets that weren't found
68        if !not_found.is_empty() {
69            printer.warning(&format!("Not found: {}", not_found.join(", ")));
70        }
71
72        // Apply --in and --by filters
73        apply_filters(&mut processes, &self.in_dir, &self.by_name);
74
75        if processes.is_empty() {
76            return Err(ProcError::ProcessNotFound(self.target.clone()));
77        }
78
79        // Dry run: just show what would be stopped
80        if self.dry_run {
81            printer.print_dry_run("stop", &processes);
82            return Ok(());
83        }
84
85        // Confirm if not --yes
86        if !printer.ask_confirm("stop", &processes, self.yes)? {
87            return Ok(());
88        }
89
90        // Parse custom signal if provided
91        #[cfg(unix)]
92        let custom_signal = if let Some(ref sig_name) = self.signal {
93            Some(parse_signal_name(sig_name)?)
94        } else {
95            None
96        };
97
98        // Stop processes
99        let mut stopped = Vec::new();
100        let mut failed = Vec::new();
101
102        for proc in &processes {
103            #[cfg(unix)]
104            let send_result = if let Some(signal) = custom_signal {
105                proc.send_signal(signal)
106            } else {
107                proc.terminate()
108            };
109            #[cfg(not(unix))]
110            let send_result = proc.terminate();
111
112            match send_result {
113                Ok(()) => {
114                    // Wait for process to exit
115                    let stopped_gracefully = self.wait_for_exit(proc);
116                    if stopped_gracefully {
117                        stopped.push(proc.clone());
118                    } else {
119                        // Force kill after timeout - use kill_and_wait for reliability
120                        match proc.kill_and_wait() {
121                            Ok(_) => stopped.push(proc.clone()),
122                            Err(e) => failed.push((proc.clone(), e.to_string())),
123                        }
124                    }
125                }
126                Err(e) => failed.push((proc.clone(), e.to_string())),
127            }
128        }
129
130        // Output results
131        printer.print_action_result("stop", &stopped, &failed);
132
133        Ok(())
134    }
135
136    fn wait_for_exit(&self, proc: &Process) -> bool {
137        let start = std::time::Instant::now();
138        let timeout = std::time::Duration::from_secs(self.timeout);
139
140        while start.elapsed() < timeout {
141            if !proc.is_running() {
142                return true;
143            }
144            std::thread::sleep(std::time::Duration::from_millis(100));
145        }
146
147        false
148    }
149}