proc_cli/commands/
stuck.rs1use 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#[derive(Args, Debug)]
18pub struct StuckCommand {
19 #[arg(long, short = 't', default_value = "300")]
21 pub timeout: u64,
22
23 #[arg(long, short = 'k')]
25 pub kill: bool,
26
27 #[arg(long, short = 'y')]
29 pub yes: bool,
30
31 #[arg(long, short = 'j')]
33 pub json: bool,
34
35 #[arg(long, short = 'v')]
37 pub verbose: bool,
38
39 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
41 pub in_dir: Option<String>,
42
43 #[arg(long = "by", short = 'b')]
45 pub by_name: Option<String>,
46}
47
48impl StuckCommand {
49 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 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 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 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}