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