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