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