Skip to main content

proc_cli/commands/
stuck.rs

1//! `proc stuck` - Find stuck/hung processes
2//!
3//! Examples:
4//!   proc stuck              # Find processes stuck > 5 minutes
5//!   proc stuck --timeout 60 # Find processes stuck > 1 minute
6//!   proc stuck --kill       # Find and kill stuck processes
7
8use crate::core::{apply_filters, Process};
9use crate::error::Result;
10use crate::ui::{plural, Printer};
11use clap::Args;
12use dialoguer::Confirm;
13use std::time::Duration;
14
15/// Find stuck/hung processes
16#[derive(Args, Debug)]
17pub struct StuckCommand {
18    /// Timeout in seconds to consider a process stuck (default: 300 = 5 minutes)
19    #[arg(long, short = 't', default_value = "300")]
20    pub timeout: u64,
21
22    /// Kill found stuck processes
23    #[arg(long, short = 'k')]
24    pub kill: bool,
25
26    /// Show what would be killed without actually killing
27    #[arg(long)]
28    pub dry_run: bool,
29
30    /// Skip confirmation when killing
31    #[arg(long, short = 'y')]
32    pub yes: bool,
33
34    /// Output as JSON
35    #[arg(long, short = 'j')]
36    pub json: bool,
37
38    /// Show verbose output
39    #[arg(long, short = 'v')]
40    pub verbose: bool,
41
42    /// Filter by directory (defaults to current directory if no path given)
43    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
44    pub in_dir: Option<String>,
45
46    /// Filter by process name
47    #[arg(long = "by", short = 'b')]
48    pub by_name: Option<String>,
49}
50
51impl StuckCommand {
52    /// Executes the stuck command, finding processes in uninterruptible states.
53    pub fn execute(&self) -> Result<()> {
54        let printer = Printer::from_flags(self.json, self.verbose);
55
56        let timeout = Duration::from_secs(self.timeout);
57        let mut processes = Process::find_stuck(timeout)?;
58
59        // Apply --in and --by filters
60        apply_filters(&mut processes, &self.in_dir, &self.by_name);
61
62        if processes.is_empty() {
63            printer.print_empty_result(
64                "stuck",
65                &format!("No stuck processes found (threshold: {}s)", self.timeout),
66            );
67            return Ok(());
68        }
69
70        printer.warning(&format!(
71            "Found {} potentially stuck process{}",
72            processes.len(),
73            plural(processes.len())
74        ));
75        printer.print_processes_as("stuck", &processes, None);
76
77        // Dry run: show what would be killed
78        if self.kill && self.dry_run {
79            printer.print_processes(&processes);
80            printer.warning(&format!(
81                "Dry run: would kill {} stuck process{}",
82                processes.len(),
83                plural(processes.len())
84            ));
85            return Ok(());
86        }
87
88        // Kill if requested
89        if self.kill {
90            if !self.yes && !self.json {
91                let confirmed = Confirm::new()
92                    .with_prompt(format!(
93                        "Kill {} stuck process{}?",
94                        processes.len(),
95                        plural(processes.len())
96                    ))
97                    .default(false)
98                    .interact()?;
99
100                if !confirmed {
101                    printer.warning("Cancelled");
102                    return Ok(());
103                }
104            }
105
106            let mut killed = Vec::new();
107            let mut failed = Vec::new();
108
109            for proc in processes {
110                // Use kill_and_wait to ensure stuck processes are actually terminated
111                match proc.kill_and_wait() {
112                    Ok(_) => killed.push(proc),
113                    Err(e) => failed.push((proc, e.to_string())),
114                }
115            }
116
117            printer.print_kill_result(&killed, &failed);
118        }
119
120        Ok(())
121    }
122}