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