proc_cli/commands/
stop.rs1use 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;
16use std::path::PathBuf;
17
18#[derive(Args, Debug)]
20pub struct StopCommand {
21 #[arg(required = true)]
23 target: String,
24
25 #[arg(long, short = 'y')]
27 yes: bool,
28
29 #[arg(long)]
31 dry_run: bool,
32
33 #[arg(long, short)]
35 json: bool,
36
37 #[arg(long, short, default_value = "10")]
39 timeout: u64,
40
41 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
43 pub in_dir: Option<String>,
44
45 #[arg(long = "by", short = 'b')]
47 pub by_name: Option<String>,
48}
49
50impl StopCommand {
51 pub fn execute(&self) -> Result<()> {
53 let format = if self.json {
54 OutputFormat::Json
55 } else {
56 OutputFormat::Human
57 };
58 let printer = Printer::new(format, false);
59
60 let targets = parse_targets(&self.target);
63 let (mut processes, not_found) = resolve_targets_excluding_self(&targets);
64
65 for target in ¬_found {
67 printer.warning(&format!("Target not found: {}", target));
68 }
69
70 let in_dir_filter = resolve_in_dir(&self.in_dir);
72 processes.retain(|p| {
73 if let Some(ref dir_path) = in_dir_filter {
74 if let Some(ref cwd) = p.cwd {
75 if !PathBuf::from(cwd).starts_with(dir_path) {
76 return false;
77 }
78 } else {
79 return false;
80 }
81 }
82 if let Some(ref name) = self.by_name {
83 if !p.name.to_lowercase().contains(&name.to_lowercase()) {
84 return false;
85 }
86 }
87 true
88 });
89
90 if processes.is_empty() {
91 return Err(ProcError::ProcessNotFound(self.target.clone()));
92 }
93
94 if self.dry_run {
96 printer.print_processes(&processes);
97 printer.warning(&format!(
98 "Dry run: would stop {} process{}",
99 processes.len(),
100 if processes.len() == 1 { "" } else { "es" }
101 ));
102 return Ok(());
103 }
104
105 if !self.yes && !self.json {
107 self.show_processes(&processes);
108
109 let prompt = format!(
110 "Stop {} process{}?",
111 processes.len(),
112 if processes.len() == 1 { "" } else { "es" }
113 );
114
115 if !Confirm::new()
116 .with_prompt(prompt)
117 .default(false)
118 .interact()?
119 {
120 printer.warning("Aborted");
121 return Ok(());
122 }
123 }
124
125 let mut stopped = Vec::new();
127 let mut failed = Vec::new();
128
129 for proc in &processes {
130 match proc.terminate() {
131 Ok(()) => {
132 let stopped_gracefully = self.wait_for_exit(proc);
134 if stopped_gracefully {
135 stopped.push(proc.clone());
136 } else {
137 match proc.kill_and_wait() {
139 Ok(_) => stopped.push(proc.clone()),
140 Err(e) => failed.push((proc.clone(), e.to_string())),
141 }
142 }
143 }
144 Err(e) => failed.push((proc.clone(), e.to_string())),
145 }
146 }
147
148 if self.json {
150 printer.print_json(&StopOutput {
151 action: "stop",
152 success: failed.is_empty(),
153 stopped_count: stopped.len(),
154 failed_count: failed.len(),
155 stopped: &stopped,
156 failed: &failed
157 .iter()
158 .map(|(p, e)| FailedStop {
159 process: p,
160 error: e,
161 })
162 .collect::<Vec<_>>(),
163 });
164 } else {
165 self.print_results(&printer, &stopped, &failed);
166 }
167
168 Ok(())
169 }
170
171 fn wait_for_exit(&self, proc: &Process) -> bool {
172 let start = std::time::Instant::now();
173 let timeout = std::time::Duration::from_secs(self.timeout);
174
175 while start.elapsed() < timeout {
176 if !proc.is_running() {
177 return true;
178 }
179 std::thread::sleep(std::time::Duration::from_millis(100));
180 }
181
182 false
183 }
184
185 fn show_processes(&self, processes: &[Process]) {
186 use colored::*;
187
188 println!(
189 "\n{} Found {} process{}:\n",
190 "!".yellow().bold(),
191 processes.len().to_string().cyan().bold(),
192 if processes.len() == 1 { "" } else { "es" }
193 );
194
195 for proc in processes {
196 println!(
197 " {} {} [PID {}] - {:.1}% CPU, {:.1} MB",
198 "→".bright_black(),
199 proc.name.white().bold(),
200 proc.pid.to_string().cyan(),
201 proc.cpu_percent,
202 proc.memory_mb
203 );
204 }
205 println!();
206 }
207
208 fn print_results(&self, printer: &Printer, stopped: &[Process], failed: &[(Process, String)]) {
209 use colored::*;
210
211 if !stopped.is_empty() {
212 println!(
213 "{} Stopped {} process{}",
214 "✓".green().bold(),
215 stopped.len().to_string().cyan().bold(),
216 if stopped.len() == 1 { "" } else { "es" }
217 );
218 for proc in stopped {
219 println!(
220 " {} {} [PID {}]",
221 "→".bright_black(),
222 proc.name.white(),
223 proc.pid.to_string().cyan()
224 );
225 }
226 }
227
228 if !failed.is_empty() {
229 printer.error(&format!(
230 "Failed to stop {} process{}",
231 failed.len(),
232 if failed.len() == 1 { "" } else { "es" }
233 ));
234 for (proc, err) in failed {
235 println!(
236 " {} {} [PID {}]: {}",
237 "→".bright_black(),
238 proc.name.white(),
239 proc.pid.to_string().cyan(),
240 err.red()
241 );
242 }
243 }
244 }
245}
246
247#[derive(Serialize)]
248struct StopOutput<'a> {
249 action: &'static str,
250 success: bool,
251 stopped_count: usize,
252 failed_count: usize,
253 stopped: &'a [Process],
254 failed: &'a [FailedStop<'a>],
255}
256
257#[derive(Serialize)]
258struct FailedStop<'a> {
259 process: &'a Process,
260 error: &'a str,
261}
262
263fn resolve_in_dir(in_dir: &Option<String>) -> Option<PathBuf> {
264 in_dir.as_ref().map(|p| {
265 if p == "." {
266 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
267 } else {
268 let path = PathBuf::from(p);
269 if path.is_relative() {
270 std::env::current_dir()
271 .unwrap_or_else(|_| PathBuf::from("."))
272 .join(path)
273 } else {
274 path
275 }
276 }
277 })
278}