proc_cli/commands/
stuck.rs1use 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#[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)]
29 pub dry_run: bool,
30
31 #[arg(long, short = 'y')]
33 pub yes: bool,
34
35 #[arg(long, short = 'j')]
37 pub json: bool,
38
39 #[arg(long, short = 'v')]
41 pub verbose: bool,
42
43 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
45 pub in_dir: Option<String>,
46
47 #[arg(long = "by", short = 'b')]
49 pub by_name: Option<String>,
50}
51
52impl StuckCommand {
53 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 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 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 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 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}