1use crate::core::{apply_filters, flatten_targets, resolve_targets, NameMatch, Process};
15use crate::error::{ExitCode, ProcError, Result};
16use crate::ui::{format_duration, plural, Printer};
17use clap::Args;
18use colored::*;
19use serde::Serialize;
20
21#[derive(Args, Debug)]
23pub struct WaitCommand {
24 #[arg(required = true, num_args = 1.., value_parser = crate::core::validate_target_arg)]
26 target: Vec<String>,
27
28 #[arg(long, short = 'n', default_value = "5")]
30 interval: u64,
31
32 #[arg(long, short = 't', default_value = "0")]
34 timeout: u64,
35
36 #[arg(long, short = 'j')]
38 json: bool,
39
40 #[arg(long, short = 'v')]
42 verbose: bool,
43
44 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
47 pub in_dir: Option<String>,
48
49 #[arg(long = "by", short = 'b')]
51 pub by_name: Option<String>,
52
53 #[arg(long)]
55 pub cmd: bool,
56
57 #[arg(long, short = 'q')]
59 quiet: bool,
60}
61
62impl WaitCommand {
63 pub fn execute(&self) -> Result<()> {
65 let printer = Printer::from_flags(self.json, self.verbose);
66
67 let interval = self.interval.max(1);
69
70 let targets = flatten_targets(&self.target);
72 let mode = NameMatch::from_flag(self.cmd);
73 let resolved = resolve_targets(&targets, mode);
74
75 if let Some(warning) = resolved.warning() {
76 printer.warning(&warning);
77 }
78 let not_found = resolved.not_found;
79 let mut processes = resolved.processes;
80
81 apply_filters(&mut processes, &self.in_dir, &self.by_name, mode);
83
84 if processes.is_empty() {
85 return Err(ProcError::ProcessNotFound(self.target.join(" ")));
86 }
87
88 if self.verbose && !self.json {
91 Process::measure_cpu(&mut processes);
92 }
93
94 let initial_count = processes.len();
95 let start = std::time::Instant::now();
96
97 if !self.json {
99 println!(
100 "{} Waiting for {} process{} to exit...",
101 "~".cyan().bold(),
102 initial_count.to_string().cyan().bold(),
103 plural(initial_count)
104 );
105 if self.verbose {
106 for proc in &processes {
107 println!(
108 " {} {} [PID {}] - {:.1}% CPU, {}",
109 "->".bright_black(),
110 proc.name.white().bold(),
111 proc.pid.to_string().cyan(),
112 proc.cpu_percent,
113 crate::ui::format_memory(proc.memory_mb)
114 );
115 }
116 }
117 }
118
119 let mut tracking: Vec<(Process, Option<u64>)> =
121 processes.into_iter().map(|p| (p, None)).collect();
122
123 let tick = std::time::Duration::from_millis(250);
127 let status_every = std::time::Duration::from_secs(interval);
128 let deadline =
129 (self.timeout > 0).then(|| start + std::time::Duration::from_secs(self.timeout));
130 let mut next_status = start + status_every;
131
132 loop {
133 let now = std::time::Instant::now();
134 let nap = match deadline {
135 Some(d) => tick.min(d.saturating_duration_since(now)),
136 None => tick,
137 };
138 std::thread::sleep(nap);
139
140 let elapsed = start.elapsed().as_secs();
141
142 for (proc, exited_at) in tracking.iter_mut() {
144 if exited_at.is_none() && !proc.is_running() {
145 *exited_at = Some(elapsed);
146 if !self.json {
147 println!(
148 "{} {} [PID {}] exited after {}",
149 "✓".green().bold(),
150 proc.name.white(),
151 proc.pid.to_string().cyan(),
152 format_duration(elapsed)
153 );
154 }
155 }
156 }
157
158 let still_running_count = tracking.iter().filter(|(_, t)| t.is_none()).count();
159
160 if still_running_count == 0 {
161 break;
162 }
163
164 if deadline.is_some_and(|d| std::time::Instant::now() >= d) {
166 let exited: Vec<ExitedProcess> = tracking
168 .iter()
169 .filter(|(_, t)| t.is_some())
170 .map(|(p, t)| ExitedProcess {
171 pid: p.pid,
172 name: p.name.clone(),
173 exited_after_seconds: t.unwrap(),
174 })
175 .collect();
176 let still_running: Vec<RunningProcess> = tracking
177 .iter()
178 .filter(|(_, t)| t.is_none())
179 .map(|(p, _)| RunningProcess {
180 pid: p.pid,
181 name: p.name.clone(),
182 })
183 .collect();
184
185 let names: Vec<String> = still_running
186 .iter()
187 .map(|p| format!("{} [{}]", p.name, p.pid))
188 .collect();
189 let message = format!(
190 "after {} — {} still running: {}",
191 format_duration(elapsed),
192 still_running.len(),
193 names.join(", ")
194 );
195
196 if self.json {
197 printer.print_json(&WaitOutput {
198 action: "wait",
199 success: false,
200 timed_out: true,
201 elapsed_seconds: elapsed,
202 elapsed_human: format_duration(elapsed),
203 target: self.target.join(" "),
204 initial_count,
205 not_found,
206 exited,
207 still_running,
208 });
209 return Err(ProcError::reported(
211 ExitCode::GeneralError,
212 format!("Operation timed out: {}", message),
213 ));
214 }
215
216 return Err(ProcError::Timeout(message));
217 }
218
219 if std::time::Instant::now() < next_status {
221 continue;
222 }
223 next_status += status_every;
224 if !self.quiet && !self.json {
225 let names: Vec<String> = tracking
226 .iter()
227 .filter(|(_, t)| t.is_none())
228 .map(|(p, _)| format!("{} [{}]", p.name, p.pid))
229 .collect();
230 let exited_count = initial_count - still_running_count;
231 let exited_note = if exited_count > 0 {
232 format!(" ({} exited)", exited_count)
233 } else {
234 String::new()
235 };
236 println!(
237 "{} {} elapsed — {} still running: {}{}",
238 "~".cyan(),
239 format_duration(elapsed),
240 still_running_count,
241 names.join(", "),
242 exited_note.bright_black()
243 );
244 }
245 }
246
247 let elapsed = start.elapsed().as_secs();
248
249 if self.json {
251 let exited: Vec<ExitedProcess> = tracking
252 .iter()
253 .map(|(p, t)| ExitedProcess {
254 pid: p.pid,
255 name: p.name.clone(),
256 exited_after_seconds: t.unwrap_or(elapsed),
257 })
258 .collect();
259 printer.print_json(&WaitOutput {
260 action: "wait",
261 success: true,
262 timed_out: false,
263 elapsed_seconds: elapsed,
264 elapsed_human: format_duration(elapsed),
265 target: self.target.join(" "),
266 initial_count,
267 not_found,
268 exited,
269 still_running: vec![],
270 });
271 } else {
272 println!(
273 "{} All {} process{} exited after {}",
274 "✓".green().bold(),
275 initial_count.to_string().cyan().bold(),
276 plural(initial_count),
277 format_duration(elapsed)
278 );
279 }
280
281 Ok(())
282 }
283}
284
285#[derive(Serialize)]
286struct WaitOutput {
287 action: &'static str,
288 success: bool,
289 timed_out: bool,
290 elapsed_seconds: u64,
291 elapsed_human: String,
292 target: String,
293 initial_count: usize,
294 not_found: Vec<String>,
296 exited: Vec<ExitedProcess>,
297 still_running: Vec<RunningProcess>,
298}
299
300#[derive(Serialize, Clone)]
301struct ExitedProcess {
302 pid: u32,
303 name: String,
304 exited_after_seconds: u64,
305}
306
307#[derive(Serialize, Clone)]
308struct RunningProcess {
309 pid: u32,
310 name: String,
311}