proc_cli/commands/
free.rs1use crate::core::port::PortInfo;
10use crate::core::{parse_target, parse_targets, Process, TargetType};
11use crate::error::{ProcError, Result};
12use crate::ui::{plural, Printer};
13use clap::Args;
14use colored::*;
15use dialoguer::Confirm;
16use serde::Serialize;
17
18#[derive(Args, Debug)]
20pub struct FreeCommand {
21 #[arg(required = true)]
23 pub target: String,
24
25 #[arg(long, short = 'y')]
27 pub yes: bool,
28
29 #[arg(long)]
31 pub dry_run: bool,
32
33 #[arg(long, short = 'j')]
35 pub json: bool,
36
37 #[arg(long, short = 'v')]
39 pub verbose: bool,
40
41 #[arg(long, default_value = "10")]
43 pub wait: u64,
44}
45
46impl FreeCommand {
47 pub fn execute(&self) -> Result<()> {
49 let printer = Printer::from_flags(self.json, self.verbose);
50
51 let targets = parse_targets(&self.target);
52
53 let mut ports: Vec<u16> = Vec::new();
55 for target in &targets {
56 match parse_target(target) {
57 TargetType::Port(port) => ports.push(port),
58 _ => {
59 return Err(ProcError::InvalidInput(format!(
60 "proc free only works with port targets (e.g. :3000), got '{}'. Use proc kill for processes.",
61 target
62 )));
63 }
64 }
65 }
66
67 let mut port_processes: Vec<(u16, Process)> = Vec::new();
69 let mut not_found: Vec<u16> = Vec::new();
70
71 for port in &ports {
72 match PortInfo::find_by_port(*port)? {
73 Some(port_info) => match Process::find_by_pid(port_info.pid)? {
74 Some(proc) => port_processes.push((*port, proc)),
75 None => not_found.push(*port),
76 },
77 None => not_found.push(*port),
78 }
79 }
80
81 for port in ¬_found {
82 printer.warning(&format!("No process listening on port {}", port));
83 }
84
85 if port_processes.is_empty() {
86 if not_found.is_empty() {
87 return Err(ProcError::InvalidInput(
88 "No port targets specified".to_string(),
89 ));
90 }
91 printer.print_empty_result("free", "All specified ports are already free");
92 return Ok(());
93 }
94
95 let processes: Vec<Process> = {
97 let mut seen = std::collections::HashSet::new();
98 port_processes
99 .iter()
100 .filter(|(_, p)| seen.insert(p.pid))
101 .map(|(_, p)| p.clone())
102 .collect()
103 };
104
105 if self.dry_run {
106 printer.print_processes(&processes);
107 printer.warning(&format!(
108 "Dry run: would kill {} process{} to free {} port{}",
109 processes.len(),
110 plural(processes.len()),
111 port_processes.len(),
112 if port_processes.len() == 1 { "" } else { "s" }
113 ));
114 return Ok(());
115 }
116
117 if !self.yes && !self.json {
118 printer.print_confirmation("free", &processes);
119
120 let prompt = format!(
121 "Kill {} process{} to free {} port{}?",
122 processes.len(),
123 plural(processes.len()),
124 port_processes.len(),
125 if port_processes.len() == 1 { "" } else { "s" }
126 );
127
128 if !Confirm::new()
129 .with_prompt(prompt)
130 .default(false)
131 .interact()?
132 {
133 printer.warning("Aborted");
134 return Ok(());
135 }
136 }
137
138 let mut kill_failed = Vec::new();
140 for proc in &processes {
141 if let Err(e) = proc.terminate() {
142 printer.warning(&format!(
143 "Failed to send SIGTERM to {} [PID {}]: {}",
144 proc.name, proc.pid, e
145 ));
146 kill_failed.push(proc.pid);
147 }
148 }
149
150 let start = std::time::Instant::now();
152 let timeout = std::time::Duration::from_secs(self.wait.min(5));
153 while start.elapsed() < timeout {
154 if processes.iter().all(|p| !p.is_running()) {
155 break;
156 }
157 std::thread::sleep(std::time::Duration::from_millis(100));
158 }
159
160 for proc in &processes {
162 if proc.is_running() {
163 if let Err(e) = proc.kill() {
164 printer.warning(&format!(
165 "Failed to kill {} [PID {}]: {}",
166 proc.name, proc.pid, e
167 ));
168 kill_failed.push(proc.pid);
169 }
170 }
171 }
172
173 let mut freed: Vec<u16> = Vec::new();
175 let mut still_busy: Vec<u16> = Vec::new();
176
177 let poll_start = std::time::Instant::now();
178 let poll_timeout = std::time::Duration::from_secs(self.wait);
179
180 let target_ports: Vec<u16> = port_processes.iter().map(|(port, _)| *port).collect();
181
182 loop {
183 let mut all_free = true;
184 freed.clear();
185 still_busy.clear();
186
187 for port in &target_ports {
188 match PortInfo::find_by_port(*port) {
189 Ok(None) => freed.push(*port),
190 _ => {
191 still_busy.push(*port);
192 all_free = false;
193 }
194 }
195 }
196
197 if all_free || poll_start.elapsed() >= poll_timeout {
198 break;
199 }
200
201 std::thread::sleep(std::time::Duration::from_millis(250));
202 }
203
204 if self.json {
206 let results: Vec<FreeResult> = freed
207 .iter()
208 .map(|p| FreeResult {
209 port: *p,
210 freed: true,
211 })
212 .chain(still_busy.iter().map(|p| FreeResult {
213 port: *p,
214 freed: false,
215 }))
216 .collect();
217
218 printer.print_json(&FreeOutput {
219 action: "free",
220 success: still_busy.is_empty(),
221 results,
222 });
223 } else {
224 for port in &freed {
225 println!(
226 "{} Freed port {}",
227 "✓".green().bold(),
228 port.to_string().cyan().bold()
229 );
230 }
231 for port in &still_busy {
232 println!(
233 "{} Port {} still in use (may be in TIME_WAIT)",
234 "✗".red().bold(),
235 port.to_string().cyan()
236 );
237 }
238 }
239
240 Ok(())
241 }
242}
243
244#[derive(Serialize)]
245struct FreeOutput {
246 action: &'static str,
247 success: bool,
248 results: Vec<FreeResult>,
249}
250
251#[derive(Serialize)]
252struct FreeResult {
253 port: u16,
254 freed: bool,
255}