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::{ExitCode, 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                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                    // Same exit code as the human-mode Timeout error
157                    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            // Check which processes have exited
167            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            // Print periodic status (unless quiet or json)
189            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        // Final output
215        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}