proc_cli/commands/
stop.rs1#[cfg(unix)]
14use crate::core::parse_signal_name;
15use crate::core::{
16 apply_filters, apply_to_each, flatten_targets, resolve_targets_excluding_self, NameMatch,
17 Process,
18};
19use crate::error::{ProcError, Result};
20use crate::ui::Printer;
21use clap::Args;
22
23#[derive(Args, Debug)]
25pub struct StopCommand {
26 #[arg(required = true, num_args = 1.., value_parser = crate::core::validate_target_arg)]
28 target: Vec<String>,
29
30 #[arg(long, short = 'y')]
32 yes: bool,
33
34 #[arg(long)]
36 dry_run: bool,
37
38 #[arg(long, short = 'j')]
40 json: bool,
41
42 #[arg(long, short = 'v')]
44 verbose: bool,
45
46 #[arg(long, short, default_value = "10")]
48 timeout: u64,
49
50 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
53 pub in_dir: Option<String>,
54
55 #[arg(long = "by", short = 'b')]
57 pub by_name: Option<String>,
58
59 #[arg(long)]
61 pub cmd: bool,
62
63 #[arg(long, short = 'S')]
65 pub signal: Option<String>,
66}
67
68impl StopCommand {
69 pub fn execute(&self) -> Result<()> {
71 let printer = Printer::from_flags(self.json, self.verbose);
72
73 #[cfg(unix)]
76 let custom_signal = if let Some(ref sig_name) = self.signal {
77 Some(parse_signal_name(sig_name)?)
78 } else {
79 None
80 };
81 #[cfg(not(unix))]
83 if self.signal.is_some() {
84 return Err(ProcError::NotSupported(
85 "stop --signal on Windows (proc stop asks the process to exit)".to_string(),
86 ));
87 }
88
89 let targets = flatten_targets(&self.target);
92 let mode = NameMatch::from_flag(self.cmd);
93 let resolved = resolve_targets_excluding_self(&targets, mode);
94
95 if let Some(warning) = resolved.warning() {
97 printer.warning(&warning);
98 }
99 let printer = printer.with_not_found(&resolved.not_found);
101 let mut processes = resolved.processes;
102
103 apply_filters(&mut processes, &self.in_dir, &self.by_name, mode);
105
106 if self.dry_run || !self.yes {
110 Process::measure_cpu(&mut processes);
111 }
112
113 if processes.is_empty() {
114 return Err(ProcError::ProcessNotFound(self.target.join(" ")));
115 }
116
117 if self.dry_run {
119 printer.print_dry_run("stop", &processes);
120 return Ok(());
121 }
122
123 if !printer.ask_confirm("stop", &processes, self.yes)? {
125 return Ok(());
126 }
127
128 let (signalled, mut failed) = apply_to_each(&processes, |proc| {
131 #[cfg(unix)]
132 if let Some(signal) = custom_signal {
133 return proc.send_signal(signal);
134 }
135 ask_to_end(proc)
136 });
137
138 let outlived = self.wait_for_exit(&signalled);
139
140 let (stopped, failed_to_kill) = apply_to_each(&signalled, |proc| {
142 if outlived.contains(&proc.pid) {
143 proc.kill_and_wait()
144 } else {
145 Ok(())
146 }
147 });
148 failed.extend(failed_to_kill);
149 in_process_order(&processes, &mut failed);
152
153 printer.print_action_result("stop", &stopped, &failed)
155 }
156
157 fn wait_for_exit(&self, processes: &[Process]) -> Vec<u32> {
159 let start = std::time::Instant::now();
160 let timeout = std::time::Duration::from_secs(self.timeout);
161 let mut running: Vec<&Process> = processes.iter().collect();
162
163 loop {
164 running.retain(|p| p.is_running());
165 if running.is_empty() || start.elapsed() >= timeout {
166 return running.iter().map(|p| p.pid).collect();
167 }
168 std::thread::sleep(std::time::Duration::from_millis(100));
169 }
170 }
171}
172
173fn in_process_order(processes: &[Process], failed: &mut [(Process, String)]) {
177 failed.sort_by_key(|(failed, _)| {
178 processes
179 .iter()
180 .position(|p| p.pid == failed.pid)
181 .unwrap_or(usize::MAX)
182 });
183}
184
185fn ask_to_end(proc: &Process) -> Result<()> {
187 match proc.terminate() {
188 Err(_) if cfg!(not(unix)) && proc.is_running() => Ok(()),
192 result => result,
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::test_support::{end, gone_record, idle_child, record_of};
200
201 #[test]
202 fn failures_from_both_passes_read_in_the_order_of_the_approved_list() {
203 let mut child = idle_child();
204 let record = record_of(&child);
205 end(&mut child);
206 let with_pid = |pid| Process {
207 pid,
208 ..record.clone()
209 };
210 let approved = [with_pid(30), with_pid(10), with_pid(20), with_pid(40)];
212 let mut failed = vec![
214 (with_pid(40), "signal".to_string()),
215 (with_pid(30), "signal".to_string()),
216 (with_pid(20), "kill".to_string()),
217 ];
218 in_process_order(&approved, &mut failed);
219 let order: Vec<(u32, &str)> = failed.iter().map(|(p, e)| (p.pid, e.as_str())).collect();
220 assert_eq!(order, [(30, "signal"), (20, "kill"), (40, "signal")]);
221 }
222
223 #[test]
224 fn a_process_that_is_gone_cannot_be_asked_to_end() {
225 let mut child = idle_child();
226 let record = gone_record(&mut child);
227 let result = ask_to_end(&record);
230 assert!(
231 matches!(&result, Err(ProcError::ProcessNotFound(_))),
232 "{result:?}"
233 );
234 }
235
236 #[test]
237 fn a_running_process_is_asked_and_the_answer_does_not_matter_on_windows() {
238 let mut child = idle_child();
239 let record = record_of(&child);
240 let result = ask_to_end(&record);
241 end(&mut child);
242 assert!(result.is_ok(), "{result:?}");
245 }
246}