1use crate::core::{apply_filters, parse_targets, resolve_targets, Process};
12use crate::error::{ProcError, Result};
13use crate::ui::{format_duration, plural, Printer};
14use clap::Args;
15use colored::*;
16use serde::Serialize;
17
18#[derive(Args, Debug)]
20pub struct WaitCommand {
21 #[arg(required = true)]
23 target: String,
24
25 #[arg(long, short = 'n', default_value = "5")]
27 interval: u64,
28
29 #[arg(long, short = 't', default_value = "0")]
31 timeout: u64,
32
33 #[arg(long, short = 'j')]
35 json: bool,
36
37 #[arg(long, short = 'v')]
39 verbose: bool,
40
41 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
43 pub in_dir: Option<String>,
44
45 #[arg(long = "by", short = 'b')]
47 pub by_name: Option<String>,
48
49 #[arg(long, short = 'q')]
51 quiet: bool,
52}
53
54impl WaitCommand {
55 pub fn execute(&self) -> Result<()> {
57 let printer = Printer::from_flags(self.json, self.verbose);
58
59 let interval = self.interval.max(1);
61
62 let targets = parse_targets(&self.target);
64 let (mut processes, not_found) = resolve_targets(&targets);
65
66 if !not_found.is_empty() {
67 printer.warning(&format!("Not found: {}", not_found.join(", ")));
68 }
69
70 apply_filters(&mut processes, &self.in_dir, &self.by_name);
72
73 if processes.is_empty() {
74 return Err(ProcError::ProcessNotFound(self.target.clone()));
75 }
76
77 let initial_count = processes.len();
78 let start = std::time::Instant::now();
79
80 if !self.json {
82 println!(
83 "{} Waiting for {} process{} to exit...",
84 "~".cyan().bold(),
85 initial_count.to_string().cyan().bold(),
86 plural(initial_count)
87 );
88 if self.verbose {
89 for proc in &processes {
90 println!(
91 " {} {} [PID {}] - {:.1}% CPU, {}",
92 "->".bright_black(),
93 proc.name.white().bold(),
94 proc.pid.to_string().cyan(),
95 proc.cpu_percent,
96 crate::ui::format_memory(proc.memory_mb)
97 );
98 }
99 }
100 }
101
102 let mut tracking: Vec<(Process, Option<u64>)> =
104 processes.into_iter().map(|p| (p, None)).collect();
105
106 loop {
108 std::thread::sleep(std::time::Duration::from_secs(interval));
109
110 let elapsed = start.elapsed().as_secs();
111
112 if self.timeout > 0 && elapsed >= self.timeout {
114 let exited: Vec<ExitedProcess> = tracking
116 .iter()
117 .filter(|(_, t)| t.is_some())
118 .map(|(p, t)| ExitedProcess {
119 pid: p.pid,
120 name: p.name.clone(),
121 exited_after_seconds: t.unwrap(),
122 })
123 .collect();
124 let still_running: Vec<RunningProcess> = tracking
125 .iter()
126 .filter(|(_, t)| t.is_none())
127 .map(|(p, _)| RunningProcess {
128 pid: p.pid,
129 name: p.name.clone(),
130 })
131 .collect();
132
133 if self.json {
134 printer.print_json(&WaitOutput {
135 action: "wait",
136 success: false,
137 timed_out: true,
138 elapsed_seconds: elapsed,
139 elapsed_human: format_duration(elapsed),
140 target: self.target.clone(),
141 initial_count,
142 exited,
143 still_running: still_running.clone(),
144 });
145 return Ok(());
146 }
147
148 let names: Vec<String> = still_running
149 .iter()
150 .map(|p| format!("{} [{}]", p.name, p.pid))
151 .collect();
152 return Err(ProcError::Timeout(format!(
153 "after {} — {} still running: {}",
154 format_duration(elapsed),
155 still_running.len(),
156 names.join(", ")
157 )));
158 }
159
160 for (proc, exited_at) in tracking.iter_mut() {
162 if exited_at.is_none() && !proc.is_running() {
163 *exited_at = Some(elapsed);
164 if !self.json {
165 println!(
166 "{} {} [PID {}] exited after {}",
167 "✓".green().bold(),
168 proc.name.white(),
169 proc.pid.to_string().cyan(),
170 format_duration(elapsed)
171 );
172 }
173 }
174 }
175
176 let still_running_count = tracking.iter().filter(|(_, t)| t.is_none()).count();
177
178 if still_running_count == 0 {
179 break;
180 }
181
182 if !self.quiet && !self.json {
184 let names: Vec<String> = tracking
185 .iter()
186 .filter(|(_, t)| t.is_none())
187 .map(|(p, _)| format!("{} [{}]", p.name, p.pid))
188 .collect();
189 let exited_count = initial_count - still_running_count;
190 let exited_note = if exited_count > 0 {
191 format!(" ({} exited)", exited_count)
192 } else {
193 String::new()
194 };
195 println!(
196 "{} {} elapsed — {} still running: {}{}",
197 "~".cyan(),
198 format_duration(elapsed),
199 still_running_count,
200 names.join(", "),
201 exited_note.bright_black()
202 );
203 }
204 }
205
206 let elapsed = start.elapsed().as_secs();
207
208 if self.json {
210 let exited: Vec<ExitedProcess> = tracking
211 .iter()
212 .map(|(p, t)| ExitedProcess {
213 pid: p.pid,
214 name: p.name.clone(),
215 exited_after_seconds: t.unwrap_or(elapsed),
216 })
217 .collect();
218 printer.print_json(&WaitOutput {
219 action: "wait",
220 success: true,
221 timed_out: false,
222 elapsed_seconds: elapsed,
223 elapsed_human: format_duration(elapsed),
224 target: self.target.clone(),
225 initial_count,
226 exited,
227 still_running: vec![],
228 });
229 } else {
230 println!(
231 "{} All {} process{} exited after {}",
232 "✓".green().bold(),
233 initial_count.to_string().cyan().bold(),
234 plural(initial_count),
235 format_duration(elapsed)
236 );
237 }
238
239 Ok(())
240 }
241}
242
243#[derive(Serialize)]
244struct WaitOutput {
245 action: &'static str,
246 success: bool,
247 timed_out: bool,
248 elapsed_seconds: u64,
249 elapsed_human: String,
250 target: String,
251 initial_count: usize,
252 exited: Vec<ExitedProcess>,
253 still_running: Vec<RunningProcess>,
254}
255
256#[derive(Serialize, Clone)]
257struct ExitedProcess {
258 pid: u32,
259 name: String,
260 exited_after_seconds: u64,
261}
262
263#[derive(Serialize, Clone)]
264struct RunningProcess {
265 pid: u32,
266 name: String,
267}