Skip to main content

proc_cli/commands/
stuck.rs

1//! `proc stuck` - Find stuck/hung processes
2//!
3//! A process counts as stuck when it has been running longer than --timeout and,
4//! in two readings a second apart, is using more than 50% CPU or is blocked in
5//! uninterruptible sleep. A long build at full CPU looks the same as a loop
6//! that never ends, so check the list before --kill.
7//!
8//! Examples:
9//!   proc stuck              # Running over 5 minutes, and busy or blocked for 2 seconds
10//!   proc stuck --timeout 60 # The same, for processes running over 1 minute
11//!   proc stuck --kill       # Find and kill stuck processes
12//!   proc stuck --by node    # Only those named node (--cmd: node anywhere in the command line)
13
14use crate::core::{apply_filters, apply_to_each, NameMatch, Process};
15use crate::error::Result;
16use crate::ui::{must_confirm, plural, prompt, Printer};
17use clap::Args;
18use std::time::Duration;
19
20/// Find stuck/hung processes
21#[derive(Args, Debug)]
22pub struct StuckCommand {
23    /// Only processes running longer than this many seconds (stuck: over 50% CPU or blocked, for 2 seconds)
24    #[arg(long, short = 't', default_value = "300")]
25    pub timeout: u64,
26
27    /// Kill found stuck processes
28    #[arg(long, short = 'k')]
29    pub kill: bool,
30
31    /// Show what would be killed without actually killing
32    #[arg(long)]
33    pub dry_run: bool,
34
35    /// Skip confirmation when killing
36    #[arg(long, short = 'y')]
37    pub yes: bool,
38
39    /// Output as JSON
40    #[arg(long, short = 'j')]
41    pub json: bool,
42
43    /// Show verbose output
44    #[arg(long, short = 'v')]
45    pub verbose: bool,
46
47    /// Print full directories and arguments instead of fitting the terminal width
48    #[arg(long, short = 'w')]
49    pub wide: bool,
50
51    /// Filter by directory; takes the next word as the directory unless it starts with '-'
52    /// (bare --in is the current directory, --in=DIR is explicit)
53    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
54    pub in_dir: Option<String>,
55
56    /// Filter by process name
57    #[arg(long = "by", short = 'b')]
58    pub by_name: Option<String>,
59
60    /// Match --by as a substring of the process name or full command line (like pkill -f)
61    #[arg(long)]
62    pub cmd: bool,
63}
64
65impl StuckCommand {
66    /// Executes the stuck command, finding long-running processes that stay busy (over 50% CPU)
67    /// or are blocked in uninterruptible sleep.
68    pub fn execute(&self) -> Result<()> {
69        let printer = Printer::from_flags(self.json, self.verbose).wide(self.wide);
70
71        let timeout = Duration::from_secs(self.timeout);
72        let mut processes = Process::find_stuck(timeout)?;
73
74        // Apply --in and --by filters
75        apply_filters(
76            &mut processes,
77            &self.in_dir,
78            &self.by_name,
79            NameMatch::from_flag(self.cmd),
80        )?;
81
82        if processes.is_empty() {
83            printer.print_empty_result(
84                "stuck",
85                &format!("No stuck processes found (threshold: {}s)", self.timeout),
86            );
87            return Ok(());
88        }
89
90        printer.warning(&format!(
91            "Found {} potentially stuck process{}",
92            processes.len(),
93            plural(processes.len())
94        ));
95
96        if !self.kill {
97            printer.print_processes_under_headline("stuck", &processes);
98            return Ok(());
99        }
100
101        // Dry run: show what would be killed
102        if self.dry_run {
103            printer.print_dry_run_with(
104                "stuck",
105                &processes,
106                &format!(
107                    "Dry run: would kill {} stuck process{}",
108                    processes.len(),
109                    plural(processes.len())
110                ),
111            );
112            return Ok(());
113        }
114
115        // Same rule everywhere: --yes, or ask; --json never skips it
116        let asking = must_confirm("stuck --kill", self.yes, self.json, &processes, None);
117
118        // Kill: JSON prints a single action result; humans see the list first.
119        // When they are asked, the question shows it, beside the question.
120        if !self.json && !matches!(asking, Ok(true)) {
121            printer.print_processes_under_headline("stuck", &processes);
122        }
123
124        if asking? {
125            let question = format!(
126                "Kill {} stuck process{}?",
127                processes.len(),
128                plural(processes.len())
129            );
130            let list = || printer.print_processes_under_headline("stuck", &processes);
131            let confirmed = prompt::ask("stuck --kill", &processes, None, &question, list)?;
132
133            if !confirmed {
134                printer.warning("Cancelled");
135                return Ok(());
136            }
137        }
138
139        // Use kill_and_wait to ensure stuck processes are actually terminated
140        let (killed, failed) = apply_to_each(&processes, Process::kill_and_wait);
141
142        printer.print_action_result_as("stuck", "kill", &killed, &failed)
143    }
144}