proc_cli/commands/
stop.rs1#[cfg(unix)]
14use crate::core::parse_signal_name;
15use crate::core::{
16 apply_to_each, flatten_targets, guard_targets, resolve_targets_excluding_self, Guards,
17 NameMatch, 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 = "start-time", value_name = "SECONDS")]
62 pub start_time: Option<u64>,
63
64 #[arg(long)]
66 pub cmd: bool,
67
68 #[arg(long, short = 'S')]
70 pub signal: Option<String>,
71}
72
73impl StopCommand {
74 pub fn execute(&self) -> Result<()> {
76 let printer = Printer::from_flags(self.json, self.verbose);
77
78 #[cfg(unix)]
81 let custom_signal = if let Some(ref sig_name) = self.signal {
82 Some(parse_signal_name(sig_name)?)
83 } else {
84 None
85 };
86 #[cfg(not(unix))]
88 if self.signal.is_some() {
89 return Err(ProcError::NotSupported(
90 "stop --signal on Windows (proc stop asks the process to exit)".to_string(),
91 ));
92 }
93
94 let targets = flatten_targets(&self.target);
97 let mode = NameMatch::from_flag(self.cmd);
98 let resolved = resolve_targets_excluding_self(&targets, mode);
99
100 if let Some(warning) = resolved.warning() {
102 printer.warning(&warning);
103 }
104 let printer = printer.with_not_found(&resolved.not_found);
106 let mut processes = resolved.processes;
107
108 let guards = Guards::new(&self.in_dir, &self.by_name, self.start_time);
110 guard_targets(&mut processes, &self.target, &guards, mode)?;
111
112 if self.dry_run || !self.yes {
116 Process::measure_cpu(&mut processes);
117 }
118
119 if processes.is_empty() {
120 return Err(ProcError::ProcessNotFound(self.target.join(" ")));
121 }
122
123 if self.dry_run {
125 printer.print_dry_run("stop", &processes);
126 return Ok(());
127 }
128
129 if !printer.ask_confirm("stop", &processes, self.yes)? {
131 return Ok(());
132 }
133
134 let (signalled, mut failed) = apply_to_each(&processes, |proc| {
137 #[cfg(unix)]
138 if let Some(signal) = custom_signal {
139 return proc.send_signal(signal);
140 }
141 ask_to_end(proc)
142 });
143
144 let outlived = self.wait_for_exit(&signalled);
145
146 let (stopped, failed_to_kill) = apply_to_each(&signalled, |proc| {
148 if outlived.contains(&proc.pid) {
149 proc.kill_and_wait()
150 } else {
151 Ok(())
152 }
153 });
154 failed.extend(failed_to_kill);
155 in_process_order(&processes, &mut failed);
158
159 printer.print_action_result("stop", &stopped, &failed)
161 }
162
163 fn wait_for_exit(&self, processes: &[Process]) -> Vec<u32> {
165 let start = std::time::Instant::now();
166 let timeout = std::time::Duration::from_secs(self.timeout);
167 let mut running: Vec<&Process> = processes.iter().collect();
168
169 loop {
170 running.retain(|p| p.is_running());
171 if running.is_empty() || start.elapsed() >= timeout {
172 return running.iter().map(|p| p.pid).collect();
173 }
174 std::thread::sleep(std::time::Duration::from_millis(100));
175 }
176 }
177}
178
179fn in_process_order(processes: &[Process], failed: &mut [(Process, String)]) {
183 failed.sort_by_key(|(failed, _)| {
184 processes
185 .iter()
186 .position(|p| p.pid == failed.pid)
187 .unwrap_or(usize::MAX)
188 });
189}
190
191fn ask_to_end(proc: &Process) -> Result<()> {
193 match proc.terminate() {
194 Err(_) if cfg!(not(unix)) && proc.is_running() => Ok(()),
198 result => result,
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::test_support::{end, gone_record, idle_child, record_of};
206
207 #[test]
208 fn failures_from_both_passes_read_in_the_order_of_the_approved_list() {
209 let mut child = idle_child();
210 let record = record_of(&child);
211 end(&mut child);
212 let with_pid = |pid| Process {
213 pid,
214 ..record.clone()
215 };
216 let approved = [with_pid(30), with_pid(10), with_pid(20), with_pid(40)];
218 let mut failed = vec![
220 (with_pid(40), "signal".to_string()),
221 (with_pid(30), "signal".to_string()),
222 (with_pid(20), "kill".to_string()),
223 ];
224 in_process_order(&approved, &mut failed);
225 let order: Vec<(u32, &str)> = failed.iter().map(|(p, e)| (p.pid, e.as_str())).collect();
226 assert_eq!(order, [(30, "signal"), (20, "kill"), (40, "signal")]);
227 }
228
229 #[test]
230 fn a_process_that_is_gone_cannot_be_asked_to_end() {
231 let mut child = idle_child();
232 let record = gone_record(&mut child);
233 let result = ask_to_end(&record);
236 assert!(
237 matches!(&result, Err(ProcError::ProcessNotFound(_))),
238 "{result:?}"
239 );
240 }
241
242 #[test]
243 fn a_running_process_is_asked_and_the_answer_does_not_matter_on_windows() {
244 let mut child = idle_child();
245 let record = record_of(&child);
246 let result = ask_to_end(&record);
247 end(&mut child);
248 assert!(result.is_ok(), "{result:?}");
251 }
252}