Skip to main content

proc_cli/commands/
wait.rs

1//! `proc wait` - Wait for process(es) to exit
2//!
3//! Examples:
4//!   proc wait node                   # Wait until all node processes exit
5//!   proc wait 1234                   # Wait for PID 1234 to exit
6//!   proc wait :3000                  # Wait until port 3000 is free
7//!   proc wait node,python            # Wait for all to exit
8//!   proc wait node --timeout 3600    # Timeout after 1 hour
9//!   proc wait node --interval 10     # Check every 10 seconds
10
11use 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/// Wait for process(es) to exit
19#[derive(Args, Debug)]
20pub struct WaitCommand {
21    /// Target(s): process name, PID, or :port (comma-separated for multiple)
22    #[arg(required = true)]
23    target: String,
24
25    /// Poll interval in seconds
26    #[arg(long, short = 'n', default_value = "5")]
27    interval: u64,
28
29    /// Timeout in seconds (0 = no timeout)
30    #[arg(long, short = 't', default_value = "0")]
31    timeout: u64,
32
33    /// Output as JSON
34    #[arg(long, short = 'j')]
35    json: bool,
36
37    /// Show verbose output
38    #[arg(long, short = 'v')]
39    verbose: bool,
40
41    /// Filter by directory (defaults to current directory if no path given)
42    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
43    pub in_dir: Option<String>,
44
45    /// Filter by process name
46    #[arg(long = "by", short = 'b')]
47    pub by_name: Option<String>,
48
49    /// Suppress periodic status messages (only print final result)
50    #[arg(long, short = 'q')]
51    quiet: bool,
52}
53
54impl WaitCommand {
55    /// Executes the wait command, blocking until all matched processes exit.
56    pub fn execute(&self) -> Result<()> {
57        let printer = Printer::from_flags(self.json, self.verbose);
58
59        // Clamp interval to minimum 1 second
60        let interval = self.interval.max(1);
61
62        // Parse and resolve targets
63        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 --in and --by filters
71        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        // Print initial status
81        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        // Track processes: (process, exited_after_seconds)
103        let mut tracking: Vec<(Process, Option<u64>)> =
104            processes.into_iter().map(|p| (p, None)).collect();
105
106        // Poll loop
107        loop {
108            std::thread::sleep(std::time::Duration::from_secs(interval));
109
110            let elapsed = start.elapsed().as_secs();
111
112            // Check timeout
113            if self.timeout > 0 && elapsed >= self.timeout {
114                // Mark still-running processes
115                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            // Check which processes have exited
161            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            // Print periodic status (unless quiet or json)
183            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        // Final output
209        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}