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 --by node    # Only those named node (--cmd: node anywhere in the command line)
7//!   proc orphans --kill       # Find and kill orphans
8//!   proc orphans --kill --yes # Kill orphans without confirmation
9
10use crate::core::Process;
11#[cfg(unix)]
12use crate::core::{apply_filters, apply_to_each, NameMatch};
13use crate::error::Result;
14#[cfg(unix)]
15use crate::ui::{must_confirm, plural, prompt, Printer};
16use clap::Args;
17
18/// Find orphaned processes (re-parented to PID 1: init/launchd; includes processes launchd started itself). Not available on Windows
19#[derive(Args, Debug)]
20pub struct OrphansCommand {
21    /// Kill found orphaned processes
22    #[arg(long, short = 'k')]
23    pub kill: bool,
24
25    /// Show what would be killed without actually killing
26    #[arg(long)]
27    pub dry_run: bool,
28
29    /// Skip confirmation when killing
30    #[arg(long, short = 'y')]
31    pub yes: bool,
32
33    /// Output as JSON
34    #[arg(long, short = 'j')]
35    pub json: bool,
36
37    /// Show verbose output
38    #[arg(long, short = 'v')]
39    pub verbose: bool,
40
41    /// Print full directories and arguments instead of fitting the terminal width
42    #[arg(long, short = 'w')]
43    pub wide: bool,
44
45    /// Filter by directory; takes the next word as the directory unless it starts with '-'
46    /// (bare --in is the current directory, --in=DIR is explicit)
47    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
48    pub in_dir: Option<String>,
49
50    /// Filter by process name
51    #[arg(long = "by", short = 'b')]
52    pub by_name: Option<String>,
53
54    /// Match --by as a substring of the process name or full command line (like pkill -f)
55    #[arg(long)]
56    pub cmd: bool,
57}
58
59impl OrphansCommand {
60    /// Executes the orphans command, finding processes whose parent is PID 1.
61    #[cfg(unix)]
62    pub fn execute(&self) -> Result<()> {
63        let printer = Printer::from_flags(self.json, self.verbose).wide(self.wide);
64
65        let mut processes = Process::find_orphans()?;
66
67        // Apply --in and --by filters
68        apply_filters(
69            &mut processes,
70            &self.in_dir,
71            &self.by_name,
72            NameMatch::from_flag(self.cmd),
73        )?;
74
75        // Real CPU readings for every view that shows or returns the list; only
76        // `--kill --json --yes` (no dry run) goes straight to killing
77        if !(self.kill && self.json && self.yes && !self.dry_run) {
78            Process::measure_cpu(&mut processes);
79        }
80
81        if processes.is_empty() {
82            printer.print_empty_result("orphans", "No orphaned processes found");
83            return Ok(());
84        }
85
86        printer.warning(&format!(
87            "Found {} orphaned process{}",
88            processes.len(),
89            plural(processes.len())
90        ));
91
92        if !self.kill {
93            printer.print_processes_under_headline("orphans", &processes);
94            return Ok(());
95        }
96
97        // Dry run: show what would be killed
98        if self.dry_run {
99            printer.print_dry_run_with(
100                "orphans",
101                &processes,
102                &format!(
103                    "Dry run: would kill {} orphaned process{}",
104                    processes.len(),
105                    plural(processes.len())
106                ),
107            );
108            return Ok(());
109        }
110
111        // Same rule everywhere: --yes, or ask; --json never skips it
112        let asking = must_confirm("orphans --kill", self.yes, self.json, &processes, None);
113
114        // Kill: JSON prints a single action result; humans see the list first.
115        // When they are asked, the question shows it, beside the question.
116        if !self.json && !matches!(asking, Ok(true)) {
117            printer.print_processes_under_headline("orphans", &processes);
118        }
119
120        if asking? {
121            let question = format!(
122                "Kill {} orphaned process{}?",
123                processes.len(),
124                plural(processes.len())
125            );
126            let list = || printer.print_processes_under_headline("orphans", &processes);
127            let confirmed = prompt::ask("orphans --kill", &processes, None, &question, list)?;
128
129            if !confirmed {
130                printer.warning("Cancelled");
131                return Ok(());
132            }
133        }
134
135        let (killed, failed) = apply_to_each(&processes, |proc| proc.kill_and_wait());
136
137        printer.print_action_result_as("orphans", "kill", &killed, &failed)
138    }
139
140    /// Windows stub: Windows does not re-parent a process whose parent exited,
141    /// so there is no reliable orphan to find (or to kill)
142    #[cfg(not(unix))]
143    pub fn execute(&self) -> Result<()> {
144        Process::find_orphans().map(|_| ())
145    }
146}