1use crate::core::{parse_targets, resolve_in_dir, resolve_targets_excluding_self, Process};
20use crate::error::{ProcError, Result};
21use crate::ui::{format_duration, OutputFormat, Printer};
22use clap::Args;
23use colored::*;
24use dialoguer::Confirm;
25use serde::Serialize;
26use std::path::PathBuf;
27use std::time::Duration;
28
29#[cfg(unix)]
30use nix::sys::signal::{kill, Signal};
31#[cfg(unix)]
32use nix::unistd::Pid;
33
34#[derive(Args, Debug)]
36pub struct UnstickCommand {
37 target: Option<String>,
39
40 #[arg(long, short, default_value = "300")]
42 timeout: u64,
43
44 #[arg(long, short = 'f')]
46 force: bool,
47
48 #[arg(long, short = 'y')]
50 yes: bool,
51
52 #[arg(long)]
54 dry_run: bool,
55
56 #[arg(long, short = 'v')]
58 verbose: bool,
59
60 #[arg(long, short = 'j')]
62 json: bool,
63
64 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
66 pub in_dir: Option<String>,
67
68 #[arg(long = "by", short = 'b')]
70 pub by_name: Option<String>,
71}
72
73#[derive(Debug, Clone, PartialEq)]
74enum Outcome {
75 Recovered, Terminated, StillStuck, NotStuck, Failed(String),
80}
81
82impl UnstickCommand {
83 pub fn execute(&self) -> Result<()> {
85 let format = if self.json {
86 OutputFormat::Json
87 } else {
88 OutputFormat::Human
89 };
90 let printer = Printer::new(format, self.verbose);
91
92 let mut stuck = if let Some(ref target) = self.target {
94 self.resolve_target_processes(target)?
96 } else {
97 let timeout = Duration::from_secs(self.timeout);
99 Process::find_stuck(timeout)?
100 };
101
102 let in_dir_filter = resolve_in_dir(&self.in_dir);
104 stuck.retain(|p| {
105 if let Some(ref dir_path) = in_dir_filter {
106 if let Some(ref cwd) = p.cwd {
107 if !PathBuf::from(cwd).starts_with(dir_path) {
108 return false;
109 }
110 } else {
111 return false;
112 }
113 }
114 if let Some(ref name) = self.by_name {
115 if !p.name.to_lowercase().contains(&name.to_lowercase()) {
116 return false;
117 }
118 }
119 true
120 });
121
122 if stuck.is_empty() {
123 if self.json {
124 printer.print_json(&UnstickOutput {
125 action: "unstick",
126 success: true,
127 dry_run: self.dry_run,
128 force: self.force,
129 found: 0,
130 recovered: 0,
131 not_stuck: 0,
132 still_stuck: 0,
133 terminated: 0,
134 failed: 0,
135 processes: Vec::new(),
136 });
137 } else if self.target.is_some() {
138 printer.warning("Target process not found");
139 } else {
140 printer.success("No stuck processes found");
141 }
142 return Ok(());
143 }
144
145 if !self.json {
147 self.show_processes(&stuck);
148 }
149
150 if self.dry_run {
152 if self.json {
153 printer.print_json(&UnstickOutput {
154 action: "unstick",
155 success: true,
156 dry_run: true,
157 force: self.force,
158 found: stuck.len(),
159 recovered: 0,
160 not_stuck: 0,
161 still_stuck: 0,
162 terminated: 0,
163 failed: 0,
164 processes: stuck
165 .iter()
166 .map(|p| ProcessOutcome {
167 pid: p.pid,
168 name: p.name.clone(),
169 outcome: "would_attempt".to_string(),
170 })
171 .collect(),
172 });
173 } else {
174 println!(
175 "\n{} Dry run: Would attempt to unstick {} process{}",
176 "ℹ".blue().bold(),
177 stuck.len().to_string().cyan().bold(),
178 if stuck.len() == 1 { "" } else { "es" }
179 );
180 if self.force {
181 println!(" With --force: will terminate if recovery fails");
182 } else {
183 println!(" Without --force: will only attempt recovery");
184 }
185 println!();
186 }
187 return Ok(());
188 }
189
190 if !self.yes && !self.json {
192 if self.force {
193 println!(
194 "\n{} With --force: processes will be terminated if recovery fails.\n",
195 "!".yellow().bold()
196 );
197 } else {
198 println!(
199 "\n{} Will attempt recovery only. Use --force to terminate if needed.\n",
200 "ℹ".blue().bold()
201 );
202 }
203
204 let prompt = format!(
205 "Unstick {} process{}?",
206 stuck.len(),
207 if stuck.len() == 1 { "" } else { "es" }
208 );
209
210 if !Confirm::new()
211 .with_prompt(prompt)
212 .default(false)
213 .interact()?
214 {
215 printer.warning("Aborted");
216 return Ok(());
217 }
218 }
219
220 let mut outcomes: Vec<(Process, Outcome)> = Vec::new();
222
223 for proc in &stuck {
224 if !self.json {
225 print!(
226 " {} {} [PID {}]... ",
227 "→".bright_black(),
228 proc.name.white(),
229 proc.pid.to_string().cyan()
230 );
231 }
232
233 let outcome = self.attempt_unstick(proc);
234
235 if !self.json {
236 match &outcome {
237 Outcome::Recovered => println!("{}", "recovered".green()),
238 Outcome::Terminated => println!("{}", "terminated".yellow()),
239 Outcome::StillStuck => println!("{}", "still stuck".red()),
240 Outcome::NotStuck => println!("{}", "not stuck".blue()),
241 Outcome::Failed(e) => println!("{}: {}", "failed".red(), e),
242 }
243 }
244
245 outcomes.push((proc.clone(), outcome));
246 }
247
248 let recovered = outcomes
250 .iter()
251 .filter(|(_, o)| *o == Outcome::Recovered)
252 .count();
253 let terminated = outcomes
254 .iter()
255 .filter(|(_, o)| *o == Outcome::Terminated)
256 .count();
257 let still_stuck = outcomes
258 .iter()
259 .filter(|(_, o)| *o == Outcome::StillStuck)
260 .count();
261 let not_stuck = outcomes
262 .iter()
263 .filter(|(_, o)| *o == Outcome::NotStuck)
264 .count();
265 let failed = outcomes
266 .iter()
267 .filter(|(_, o)| matches!(o, Outcome::Failed(_)))
268 .count();
269
270 if self.json {
272 printer.print_json(&UnstickOutput {
273 action: "unstick",
274 success: failed == 0 && still_stuck == 0,
275 dry_run: false,
276 force: self.force,
277 found: stuck.len(),
278 recovered,
279 not_stuck,
280 still_stuck,
281 terminated,
282 failed,
283 processes: outcomes
284 .iter()
285 .map(|(p, o)| ProcessOutcome {
286 pid: p.pid,
287 name: p.name.clone(),
288 outcome: match o {
289 Outcome::Recovered => "recovered".to_string(),
290 Outcome::Terminated => "terminated".to_string(),
291 Outcome::StillStuck => "still_stuck".to_string(),
292 Outcome::NotStuck => "not_stuck".to_string(),
293 Outcome::Failed(e) => format!("failed: {}", e),
294 },
295 })
296 .collect(),
297 });
298 } else {
299 println!();
300 if recovered > 0 {
301 println!(
302 "{} {} process{} recovered",
303 "✓".green().bold(),
304 recovered.to_string().cyan().bold(),
305 if recovered == 1 { "" } else { "es" }
306 );
307 }
308 if not_stuck > 0 {
309 println!(
310 "{} {} process{} not stuck",
311 "ℹ".blue().bold(),
312 not_stuck.to_string().cyan().bold(),
313 if not_stuck == 1 { " was" } else { "es were" }
314 );
315 }
316 if terminated > 0 {
317 println!(
318 "{} {} process{} terminated",
319 "!".yellow().bold(),
320 terminated.to_string().cyan().bold(),
321 if terminated == 1 { "" } else { "es" }
322 );
323 }
324 if still_stuck > 0 {
325 println!(
326 "{} {} process{} still stuck (use --force to terminate)",
327 "✗".red().bold(),
328 still_stuck.to_string().cyan().bold(),
329 if still_stuck == 1 { "" } else { "es" }
330 );
331 }
332 if failed > 0 {
333 println!(
334 "{} {} process{} failed",
335 "✗".red().bold(),
336 failed.to_string().cyan().bold(),
337 if failed == 1 { "" } else { "es" }
338 );
339 }
340 }
341
342 Ok(())
343 }
344
345 fn resolve_target_processes(&self, target: &str) -> Result<Vec<Process>> {
347 let targets = parse_targets(target);
348 let (processes, _) = resolve_targets_excluding_self(&targets);
349 if processes.is_empty() {
350 Err(ProcError::ProcessNotFound(target.to_string()))
351 } else {
352 Ok(processes)
353 }
354 }
355
356 fn is_stuck(&self, proc: &Process) -> bool {
358 proc.cpu_percent > 50.0
359 }
360
361 #[cfg(unix)]
363 fn attempt_unstick(&self, proc: &Process) -> Outcome {
364 if self.target.is_some() && !self.is_stuck(proc) {
366 return Outcome::NotStuck;
367 }
368
369 let pid = Pid::from_raw(proc.pid as i32);
370
371 let _ = kill(pid, Signal::SIGCONT);
373 std::thread::sleep(Duration::from_secs(1));
374
375 if self.check_recovered(proc) {
376 return Outcome::Recovered;
377 }
378
379 if kill(pid, Signal::SIGINT).is_err() && !proc.is_running() {
381 return Outcome::Terminated;
382 }
383 std::thread::sleep(Duration::from_secs(3));
384
385 if !proc.is_running() {
386 return Outcome::Terminated;
387 }
388 if self.check_recovered(proc) {
389 return Outcome::Recovered;
390 }
391
392 if !self.force {
394 return Outcome::StillStuck;
395 }
396
397 if proc.terminate().is_err() && !proc.is_running() {
399 return Outcome::Terminated;
400 }
401 std::thread::sleep(Duration::from_secs(5));
402
403 if !proc.is_running() {
404 return Outcome::Terminated;
405 }
406
407 match proc.kill() {
409 Ok(()) => Outcome::Terminated,
410 Err(e) => {
411 if !proc.is_running() {
412 Outcome::Terminated
413 } else {
414 Outcome::Failed(e.to_string())
415 }
416 }
417 }
418 }
419
420 #[cfg(not(unix))]
421 fn attempt_unstick(&self, proc: &Process) -> Outcome {
422 if self.target.is_some() && !self.is_stuck(proc) {
424 return Outcome::NotStuck;
425 }
426
427 if !self.force {
429 return Outcome::StillStuck;
430 }
431
432 if proc.terminate().is_ok() {
433 std::thread::sleep(Duration::from_secs(3));
434 if !proc.is_running() {
435 return Outcome::Terminated;
436 }
437 }
438
439 match proc.kill() {
440 Ok(()) => Outcome::Terminated,
441 Err(e) => Outcome::Failed(e.to_string()),
442 }
443 }
444
445 #[cfg(unix)]
447 fn check_recovered(&self, proc: &Process) -> bool {
448 if let Ok(Some(current)) = Process::find_by_pid(proc.pid) {
449 current.cpu_percent < 10.0
450 } else {
451 false
452 }
453 }
454
455 fn show_processes(&self, processes: &[Process]) {
456 let label = if self.target.is_some() {
457 "Target"
458 } else {
459 "Found stuck"
460 };
461
462 println!(
463 "\n{} {} {} process{}:\n",
464 "!".yellow().bold(),
465 label,
466 processes.len().to_string().cyan().bold(),
467 if processes.len() == 1 { "" } else { "es" }
468 );
469
470 for proc in processes {
471 let uptime = proc
472 .start_time
473 .map(|st| {
474 let now = std::time::SystemTime::now()
475 .duration_since(std::time::UNIX_EPOCH)
476 .map(|d| d.as_secs().saturating_sub(st))
477 .unwrap_or(0);
478 format_duration(now)
479 })
480 .unwrap_or_else(|| "unknown".to_string());
481
482 println!(
483 " {} {} [PID {}] - {:.1}% CPU, running for {}",
484 "→".bright_black(),
485 proc.name.white().bold(),
486 proc.pid.to_string().cyan(),
487 proc.cpu_percent,
488 uptime.yellow()
489 );
490 }
491 }
492}
493
494#[derive(Serialize)]
495struct UnstickOutput {
496 action: &'static str,
497 success: bool,
498 dry_run: bool,
499 force: bool,
500 found: usize,
501 recovered: usize,
502 not_stuck: usize,
503 still_stuck: usize,
504 terminated: usize,
505 failed: usize,
506 processes: Vec<ProcessOutcome>,
507}
508
509#[derive(Serialize)]
510struct ProcessOutcome {
511 pid: u32,
512 name: String,
513 outcome: String,
514}