1use crate::core::{apply_filters, parse_targets, resolve_targets, Process};
12use crate::error::{ExitCode, 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 let names: Vec<String> = still_running
134 .iter()
135 .map(|p| format!("{} [{}]", p.name, p.pid))
136 .collect();
137 let message = format!(
138 "after {} — {} still running: {}",
139 format_duration(elapsed),
140 still_running.len(),
141 names.join(", ")
142 );
143
144 if self.json {
145 printer.print_json(&WaitOutput {
146 action: "wait",
147 success: false,
148 timed_out: true,
149 elapsed_seconds: elapsed,
150 elapsed_human: format_duration(elapsed),
151 target: self.target.clone(),
152 initial_count,
153 exited,
154 still_running,
155 });
156 return Err(ProcError::reported(
158 ExitCode::GeneralError,
159 format!("Operation timed out: {}", message),
160 ));
161 }
162
163 return Err(ProcError::Timeout(message));
164 }
165
166 for (proc, exited_at) in tracking.iter_mut() {
168 if exited_at.is_none() && !proc.is_running() {
169 *exited_at = Some(elapsed);
170 if !self.json {
171 println!(
172 "{} {} [PID {}] exited after {}",
173 "✓".green().bold(),
174 proc.name.white(),
175 proc.pid.to_string().cyan(),
176 format_duration(elapsed)
177 );
178 }
179 }
180 }
181
182 let still_running_count = tracking.iter().filter(|(_, t)| t.is_none()).count();
183
184 if still_running_count == 0 {
185 break;
186 }
187
188 if !self.quiet && !self.json {
190 let names: Vec<String> = tracking
191 .iter()
192 .filter(|(_, t)| t.is_none())
193 .map(|(p, _)| format!("{} [{}]", p.name, p.pid))
194 .collect();
195 let exited_count = initial_count - still_running_count;
196 let exited_note = if exited_count > 0 {
197 format!(" ({} exited)", exited_count)
198 } else {
199 String::new()
200 };
201 println!(
202 "{} {} elapsed — {} still running: {}{}",
203 "~".cyan(),
204 format_duration(elapsed),
205 still_running_count,
206 names.join(", "),
207 exited_note.bright_black()
208 );
209 }
210 }
211
212 let elapsed = start.elapsed().as_secs();
213
214 if self.json {
216 let exited: Vec<ExitedProcess> = tracking
217 .iter()
218 .map(|(p, t)| ExitedProcess {
219 pid: p.pid,
220 name: p.name.clone(),
221 exited_after_seconds: t.unwrap_or(elapsed),
222 })
223 .collect();
224 printer.print_json(&WaitOutput {
225 action: "wait",
226 success: true,
227 timed_out: false,
228 elapsed_seconds: elapsed,
229 elapsed_human: format_duration(elapsed),
230 target: self.target.clone(),
231 initial_count,
232 exited,
233 still_running: vec![],
234 });
235 } else {
236 println!(
237 "{} All {} process{} exited after {}",
238 "✓".green().bold(),
239 initial_count.to_string().cyan().bold(),
240 plural(initial_count),
241 format_duration(elapsed)
242 );
243 }
244
245 Ok(())
246 }
247}
248
249#[derive(Serialize)]
250struct WaitOutput {
251 action: &'static str,
252 success: bool,
253 timed_out: bool,
254 elapsed_seconds: u64,
255 elapsed_human: String,
256 target: String,
257 initial_count: usize,
258 exited: Vec<ExitedProcess>,
259 still_running: Vec<RunningProcess>,
260}
261
262#[derive(Serialize, Clone)]
263struct ExitedProcess {
264 pid: u32,
265 name: String,
266 exited_after_seconds: u64,
267}
268
269#[derive(Serialize, Clone)]
270struct RunningProcess {
271 pid: u32,
272 name: String,
273}