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