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_in_dir, resolve_targets_excluding_self, Process};
11use crate::error::{ProcError, Result};
12use crate::ui::{OutputFormat, Printer};
13use clap::Args;
14use dialoguer::Confirm;
15use std::path::PathBuf;
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
53impl StopCommand {
54    /// Executes the stop command, gracefully terminating matched processes.
55    pub fn execute(&self) -> Result<()> {
56        let format = if self.json {
57            OutputFormat::Json
58        } else {
59            OutputFormat::Human
60        };
61        let printer = Printer::new(format, self.verbose);
62
63        // Parse comma-separated targets and resolve to processes
64        // Use resolve_targets_excluding_self to avoid stopping ourselves
65        let targets = parse_targets(&self.target);
66        let (mut processes, not_found) = resolve_targets_excluding_self(&targets);
67
68        // Warn about targets that weren't found
69        for target in &not_found {
70            printer.warning(&format!("Target not found: {}", target));
71        }
72
73        // Apply --in and --by filters
74        let in_dir_filter = resolve_in_dir(&self.in_dir);
75        processes.retain(|p| {
76            if let Some(ref dir_path) = in_dir_filter {
77                if let Some(ref cwd) = p.cwd {
78                    if !PathBuf::from(cwd).starts_with(dir_path) {
79                        return false;
80                    }
81                } else {
82                    return false;
83                }
84            }
85            if let Some(ref name) = self.by_name {
86                if !p.name.to_lowercase().contains(&name.to_lowercase()) {
87                    return false;
88                }
89            }
90            true
91        });
92
93        if processes.is_empty() {
94            return Err(ProcError::ProcessNotFound(self.target.clone()));
95        }
96
97        // Dry run: just show what would be stopped
98        if self.dry_run {
99            printer.print_processes(&processes);
100            printer.warning(&format!(
101                "Dry run: would stop {} process{}",
102                processes.len(),
103                if processes.len() == 1 { "" } else { "es" }
104            ));
105            return Ok(());
106        }
107
108        // Confirm if not --yes
109        if !self.yes && !self.json {
110            printer.print_confirmation("stop", &processes);
111
112            let prompt = format!(
113                "Stop {} process{}?",
114                processes.len(),
115                if processes.len() == 1 { "" } else { "es" }
116            );
117
118            if !Confirm::new()
119                .with_prompt(prompt)
120                .default(false)
121                .interact()?
122            {
123                printer.warning("Aborted");
124                return Ok(());
125            }
126        }
127
128        // Stop processes
129        let mut stopped = Vec::new();
130        let mut failed = Vec::new();
131
132        for proc in &processes {
133            match proc.terminate() {
134                Ok(()) => {
135                    // Wait for process to exit
136                    let stopped_gracefully = self.wait_for_exit(proc);
137                    if stopped_gracefully {
138                        stopped.push(proc.clone());
139                    } else {
140                        // Force kill after timeout - use kill_and_wait for reliability
141                        match proc.kill_and_wait() {
142                            Ok(_) => stopped.push(proc.clone()),
143                            Err(e) => failed.push((proc.clone(), e.to_string())),
144                        }
145                    }
146                }
147                Err(e) => failed.push((proc.clone(), e.to_string())),
148            }
149        }
150
151        // Output results
152        printer.print_action_result("Stopped", &stopped, &failed);
153
154        Ok(())
155    }
156
157    fn wait_for_exit(&self, proc: &Process) -> bool {
158        let start = std::time::Instant::now();
159        let timeout = std::time::Duration::from_secs(self.timeout);
160
161        while start.elapsed() < timeout {
162            if !proc.is_running() {
163                return true;
164            }
165            std::thread::sleep(std::time::Duration::from_millis(100));
166        }
167
168        false
169    }
170}