Skip to main content

proc_cli/commands/
orphans.rs

1//! `proc orphans` - Find orphaned processes
2//!
3//! Examples:
4//!   proc orphans              # List orphaned processes
5//!   proc orphans --in .       # Orphans in current directory
6//!   proc orphans --kill       # Find and kill orphans
7//!   proc orphans --kill --yes # Kill orphans without confirmation
8
9use crate::core::{resolve_in_dir, Process};
10use crate::error::Result;
11use crate::ui::{OutputFormat, Printer};
12use clap::Args;
13use dialoguer::Confirm;
14use std::path::PathBuf;
15
16/// Find orphaned processes (parent has exited)
17#[derive(Args, Debug)]
18pub struct OrphansCommand {
19    /// Kill found orphaned processes
20    #[arg(long, short = 'k')]
21    pub kill: bool,
22
23    /// Show what would be killed without actually killing
24    #[arg(long)]
25    pub dry_run: bool,
26
27    /// Skip confirmation when killing
28    #[arg(long, short = 'y')]
29    pub yes: bool,
30
31    /// Output as JSON
32    #[arg(long, short = 'j')]
33    pub json: bool,
34
35    /// Show verbose output
36    #[arg(long, short = 'v')]
37    pub verbose: bool,
38
39    /// Filter by directory (defaults to current directory if no path given)
40    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
41    pub in_dir: Option<String>,
42
43    /// Filter by process name
44    #[arg(long = "by", short = 'b')]
45    pub by_name: Option<String>,
46}
47
48impl OrphansCommand {
49    /// Executes the orphans command, finding processes whose parent has exited.
50    pub fn execute(&self) -> Result<()> {
51        let format = if self.json {
52            OutputFormat::Json
53        } else {
54            OutputFormat::Human
55        };
56        let printer = Printer::new(format, self.verbose);
57
58        let mut processes = Process::find_orphans()?;
59
60        // Apply --in and --by filters
61        let in_dir_filter = resolve_in_dir(&self.in_dir);
62        processes.retain(|p| {
63            if let Some(ref dir_path) = in_dir_filter {
64                if let Some(ref cwd) = p.cwd {
65                    if !PathBuf::from(cwd).starts_with(dir_path) {
66                        return false;
67                    }
68                } else {
69                    return false;
70                }
71            }
72            if let Some(ref name) = self.by_name {
73                if !p.name.to_lowercase().contains(&name.to_lowercase()) {
74                    return false;
75                }
76            }
77            true
78        });
79
80        if processes.is_empty() {
81            printer.success("No orphaned processes found");
82            return Ok(());
83        }
84
85        printer.warning(&format!(
86            "Found {} orphaned process{}",
87            processes.len(),
88            if processes.len() == 1 { "" } else { "es" }
89        ));
90        printer.print_processes(&processes);
91
92        // Dry run: show what would be killed
93        if self.kill && self.dry_run {
94            printer.warning(&format!(
95                "Dry run: would kill {} orphaned process{}",
96                processes.len(),
97                if processes.len() == 1 { "" } else { "es" }
98            ));
99            return Ok(());
100        }
101
102        // Kill if requested
103        if self.kill {
104            if !self.yes && !self.json {
105                let confirmed = Confirm::new()
106                    .with_prompt(format!(
107                        "Kill {} orphaned process{}?",
108                        processes.len(),
109                        if processes.len() == 1 { "" } else { "es" }
110                    ))
111                    .default(false)
112                    .interact()
113                    .unwrap_or(false);
114
115                if !confirmed {
116                    printer.warning("Cancelled");
117                    return Ok(());
118                }
119            }
120
121            let mut killed = Vec::new();
122            let mut failed = Vec::new();
123
124            for proc in processes {
125                match proc.kill_and_wait() {
126                    Ok(_) => killed.push(proc),
127                    Err(e) => failed.push((proc, e.to_string())),
128                }
129            }
130
131            printer.print_kill_result(&killed, &failed);
132        }
133
134        Ok(())
135    }
136}