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
76        if !self.kill {
77            printer.print_processes_as("stuck", &processes, None);
78            return Ok(());
79        }
80
81        // Dry run: show what would be killed
82        if self.dry_run {
83            printer.print_dry_run_with(
84                "stuck",
85                &processes,
86                &format!(
87                    "Dry run: would kill {} stuck process{}",
88                    processes.len(),
89                    plural(processes.len())
90                ),
91            );
92            return Ok(());
93        }
94
95        // Kill: JSON prints a single action result; humans see the list first
96        if !self.json {
97            printer.print_processes_as("stuck", &processes, None);
98        }
99
100        if !self.yes && !self.json {
101            let confirmed = Confirm::new()
102                .with_prompt(format!(
103                    "Kill {} stuck process{}?",
104                    processes.len(),
105                    plural(processes.len())
106                ))
107                .default(false)
108                .interact()?;
109
110            if !confirmed {
111                printer.warning("Cancelled");
112                return Ok(());
113            }
114        }
115
116        let mut killed = Vec::new();
117        let mut failed = Vec::new();
118
119        for proc in processes {
120            // Use kill_and_wait to ensure stuck processes are actually terminated
121            match proc.kill_and_wait() {
122                Ok(_) => killed.push(proc),
123                Err(e) => failed.push((proc, e.to_string())),
124            }
125        }
126
127        printer.print_action_result_as("stuck", "kill", &killed, &failed)
128    }
129}