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