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
8use crate::core::{resolve_target, Process};
9use crate::error::{ProcError, Result};
10use crate::ui::{OutputFormat, Printer};
11use clap::Args;
12use dialoguer::Confirm;
13use serde::Serialize;
14
15/// Stop process(es) gracefully with SIGTERM
16#[derive(Args, Debug)]
17pub struct StopCommand {
18    /// Target: process name, PID, or :port
19    #[arg(required = true)]
20    target: String,
21
22    /// Skip confirmation prompt
23    #[arg(long, short = 'y')]
24    yes: bool,
25
26    /// Output as JSON
27    #[arg(long, short)]
28    json: bool,
29
30    /// Timeout in seconds to wait before force kill
31    #[arg(long, short, default_value = "10")]
32    timeout: u64,
33}
34
35impl StopCommand {
36    pub fn execute(&self) -> Result<()> {
37        let format = if self.json {
38            OutputFormat::Json
39        } else {
40            OutputFormat::Human
41        };
42        let printer = Printer::new(format, false);
43
44        // Parse target
45        let processes = self.find_target_processes()?;
46
47        if processes.is_empty() {
48            return Err(ProcError::ProcessNotFound(self.target.clone()));
49        }
50
51        // Confirm if not --yes
52        if !self.yes && !self.json {
53            self.show_processes(&processes);
54
55            let prompt = format!(
56                "Stop {} process{}?",
57                processes.len(),
58                if processes.len() == 1 { "" } else { "es" }
59            );
60
61            if !Confirm::new()
62                .with_prompt(prompt)
63                .default(false)
64                .interact()?
65            {
66                printer.warning("Aborted");
67                return Ok(());
68            }
69        }
70
71        // Stop processes
72        let mut stopped = Vec::new();
73        let mut failed = Vec::new();
74
75        for proc in &processes {
76            match proc.terminate() {
77                Ok(()) => {
78                    // Wait for process to exit
79                    let stopped_gracefully = self.wait_for_exit(proc);
80                    if stopped_gracefully {
81                        stopped.push(proc.clone());
82                    } else {
83                        // Force kill after timeout - use kill_and_wait for reliability
84                        match proc.kill_and_wait() {
85                            Ok(_) => stopped.push(proc.clone()),
86                            Err(e) => failed.push((proc.clone(), e.to_string())),
87                        }
88                    }
89                }
90                Err(e) => failed.push((proc.clone(), e.to_string())),
91            }
92        }
93
94        // Output results
95        if self.json {
96            printer.print_json(&StopOutput {
97                action: "stop",
98                success: failed.is_empty(),
99                stopped_count: stopped.len(),
100                failed_count: failed.len(),
101                stopped: &stopped,
102                failed: &failed
103                    .iter()
104                    .map(|(p, e)| FailedStop {
105                        process: p,
106                        error: e,
107                    })
108                    .collect::<Vec<_>>(),
109            });
110        } else {
111            self.print_results(&printer, &stopped, &failed);
112        }
113
114        Ok(())
115    }
116
117    fn find_target_processes(&self) -> Result<Vec<Process>> {
118        resolve_target(&self.target)
119    }
120
121    fn wait_for_exit(&self, proc: &Process) -> bool {
122        let start = std::time::Instant::now();
123        let timeout = std::time::Duration::from_secs(self.timeout);
124
125        while start.elapsed() < timeout {
126            if !proc.is_running() {
127                return true;
128            }
129            std::thread::sleep(std::time::Duration::from_millis(100));
130        }
131
132        false
133    }
134
135    fn show_processes(&self, processes: &[Process]) {
136        use colored::*;
137
138        println!(
139            "\n{} Found {} process{}:\n",
140            "!".yellow().bold(),
141            processes.len().to_string().cyan().bold(),
142            if processes.len() == 1 { "" } else { "es" }
143        );
144
145        for proc in processes {
146            println!(
147                "  {} {} [PID {}] - {:.1}% CPU, {:.1} MB",
148                "→".bright_black(),
149                proc.name.white().bold(),
150                proc.pid.to_string().cyan(),
151                proc.cpu_percent,
152                proc.memory_mb
153            );
154        }
155        println!();
156    }
157
158    fn print_results(&self, printer: &Printer, stopped: &[Process], failed: &[(Process, String)]) {
159        use colored::*;
160
161        if !stopped.is_empty() {
162            println!(
163                "{} Stopped {} process{}",
164                "✓".green().bold(),
165                stopped.len().to_string().cyan().bold(),
166                if stopped.len() == 1 { "" } else { "es" }
167            );
168            for proc in stopped {
169                println!(
170                    "  {} {} [PID {}]",
171                    "→".bright_black(),
172                    proc.name.white(),
173                    proc.pid.to_string().cyan()
174                );
175            }
176        }
177
178        if !failed.is_empty() {
179            printer.error(&format!(
180                "Failed to stop {} process{}",
181                failed.len(),
182                if failed.len() == 1 { "" } else { "es" }
183            ));
184            for (proc, err) in failed {
185                println!(
186                    "  {} {} [PID {}]: {}",
187                    "→".bright_black(),
188                    proc.name.white(),
189                    proc.pid.to_string().cyan(),
190                    err.red()
191                );
192            }
193        }
194    }
195}
196
197#[derive(Serialize)]
198struct StopOutput<'a> {
199    action: &'static str,
200    success: bool,
201    stopped_count: usize,
202    failed_count: usize,
203    stopped: &'a [Process],
204    failed: &'a [FailedStop<'a>],
205}
206
207#[derive(Serialize)]
208struct FailedStop<'a> {
209    process: &'a Process,
210    error: &'a str,
211}