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