1use crate::core::port::PortInfo;
12use crate::core::{flatten_targets, parse_target, Process, TargetType};
13use crate::error::{ExitCode, ProcError, Result};
14use crate::ui::{must_confirm, plural, Printer};
15use clap::Args;
16use colored::*;
17use dialoguer::Confirm;
18use serde::Serialize;
19
20#[derive(Args, Debug)]
22pub struct FreeCommand {
23 #[arg(required = true, num_args = 1.., value_parser = crate::core::validate_target_arg)]
25 pub target: Vec<String>,
26
27 #[arg(long, short = 'y')]
29 pub yes: bool,
30
31 #[arg(long)]
33 pub dry_run: 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, default_value = "10")]
45 pub wait: u64,
46}
47
48impl FreeCommand {
49 pub fn execute(&self) -> Result<()> {
51 let printer = Printer::from_flags(self.json, self.verbose);
52
53 let targets = flatten_targets(&self.target);
54
55 let mut ports: Vec<(&String, u16)> = Vec::new();
57 for target in &targets {
58 match parse_target(target) {
59 TargetType::Port(port) => ports.push((target, port)),
60 _ => {
61 return Err(ProcError::InvalidInput(format!(
62 "proc free only works with port targets (e.g. :3000), got '{}'. Use proc kill for processes.",
63 target
64 )));
65 }
66 }
67 }
68
69 let mut port_processes: Vec<(u16, Process)> = Vec::new();
71 let mut not_found: Vec<String> = Vec::new();
73
74 for (target, port) in &ports {
75 let mut held = false;
77 for listener in PortInfo::holders_of(*port)? {
78 if let Some(proc) = Process::find_by_pid(listener.pid)? {
79 port_processes.push((*port, proc));
80 held = true;
81 }
82 }
83 if !held {
84 printer.warning(&format!("No process listening on port {}", port));
85 not_found.push((*target).clone());
86 }
87 }
88 let printer = printer.with_not_found(¬_found);
90
91 if port_processes.is_empty() {
92 if not_found.is_empty() {
93 return Err(ProcError::InvalidInput(
94 "No port targets specified".to_string(),
95 ));
96 }
97 printer.print_empty_result("free", "All specified ports are already free");
98 return Ok(());
99 }
100
101 let target_ports: Vec<u16> = {
103 let mut seen = std::collections::HashSet::new();
104 port_processes
105 .iter()
106 .map(|(port, _)| *port)
107 .filter(|port| seen.insert(*port))
108 .collect()
109 };
110
111 let mut processes: Vec<Process> = {
113 let mut seen = std::collections::HashSet::new();
114 port_processes
115 .iter()
116 .filter(|(_, p)| seen.insert(p.pid))
117 .map(|(_, p)| p.clone())
118 .collect()
119 };
120
121 if self.dry_run || !self.yes {
124 Process::measure_cpu(&mut processes);
125 }
126
127 if self.dry_run {
128 printer.print_dry_run_with(
129 "free",
130 &processes,
131 &format!(
132 "Dry run: would kill {} process{} to free {} port{}",
133 processes.len(),
134 plural(processes.len()),
135 target_ports.len(),
136 if target_ports.len() == 1 { "" } else { "s" }
137 ),
138 );
139 return Ok(());
140 }
141
142 if must_confirm("free", self.yes, self.json, &processes, Some(¬_found))? {
144 printer.print_confirmation("free", &processes);
145
146 let prompt = format!(
147 "Kill {} process{} to free {} port{}?",
148 processes.len(),
149 plural(processes.len()),
150 target_ports.len(),
151 if target_ports.len() == 1 { "" } else { "s" }
152 );
153
154 if !Confirm::new()
155 .with_prompt(prompt)
156 .default(false)
157 .interact()?
158 {
159 printer.warning("Aborted");
160 return Ok(());
161 }
162 }
163
164 let start = std::time::Instant::now();
166 stop_all(
167 &processes,
168 std::time::Duration::from_secs(self.wait.min(5)),
169 &printer,
170 Process::terminate,
171 Process::kill,
172 );
173
174 let mut freed: Vec<u16> = Vec::new();
176 let mut still_busy: Vec<u16> = Vec::new();
177
178 let poll_deadline = std::cmp::max(
181 start + std::time::Duration::from_secs(self.wait),
182 std::time::Instant::now() + std::time::Duration::from_secs(1),
183 );
184
185 let mut was_free = false;
190 loop {
191 let mut all_free = true;
192 freed.clear();
193 still_busy.clear();
194
195 for port in &target_ports {
196 match PortInfo::holders_of(*port) {
198 Ok(holders) if holders.is_empty() => freed.push(*port),
199 _ => {
200 still_busy.push(*port);
201 all_free = false;
202 }
203 }
204 }
205
206 let settled = all_free && was_free;
207 was_free = all_free;
208 if settled || std::time::Instant::now() >= poll_deadline {
209 break;
210 }
211
212 std::thread::sleep(std::time::Duration::from_millis(250));
213 }
214
215 if self.json {
217 let results: Vec<FreeResult> = freed
218 .iter()
219 .map(|p| FreeResult {
220 port: *p,
221 freed: true,
222 })
223 .chain(still_busy.iter().map(|p| FreeResult {
224 port: *p,
225 freed: false,
226 }))
227 .collect();
228
229 printer.print_json(&FreeOutput {
230 action: "free",
231 success: still_busy.is_empty(),
232 results,
233 not_found: ¬_found,
234 });
235 } else {
236 for port in &freed {
237 println!(
238 "{} Freed port {}",
239 "✓".green().bold(),
240 port.to_string().cyan().bold()
241 );
242 }
243 for port in &still_busy {
244 println!(
245 "{} Port {} still in use (may be in TIME_WAIT)",
246 "✗".red().bold(),
247 port.to_string().cyan()
248 );
249 }
250 }
251
252 if still_busy.is_empty() {
253 Ok(())
254 } else {
255 Err(ProcError::reported(
256 ExitCode::GeneralError,
257 format!(
258 "{} port{} still in use",
259 still_busy.len(),
260 if still_busy.len() == 1 { "" } else { "s" }
261 ),
262 ))
263 }
264 }
265}
266
267#[derive(Serialize)]
268struct FreeOutput<'a> {
269 action: &'static str,
270 success: bool,
271 results: Vec<FreeResult>,
272 not_found: &'a [String],
274}
275
276#[derive(Serialize)]
277struct FreeResult {
278 port: u16,
279 freed: bool,
280}
281
282fn stop_all(
289 processes: &[Process],
290 grace: std::time::Duration,
291 printer: &Printer,
292 terminate: impl Fn(&Process) -> Result<()>,
293 kill: impl Fn(&Process) -> Result<()>,
294) -> Vec<u32> {
295 let mut failed = Vec::new();
296 for proc in processes {
297 match terminate(proc) {
298 Err(_) if cfg!(not(unix)) && proc.is_running() => {}
301 Err(e) => {
302 printer.warning(&format!(
303 "Failed to ask {} [PID {}] to stop: {}",
304 proc.name, proc.pid, e
305 ));
306 failed.push(proc.pid);
307 }
308 Ok(()) => {}
309 }
310 }
311
312 let start = std::time::Instant::now();
314 while start.elapsed() < grace {
315 if processes.iter().all(|p| !p.is_running()) {
316 break;
317 }
318 std::thread::sleep(std::time::Duration::from_millis(100));
319 }
320
321 for proc in processes {
323 if proc.is_running() {
324 if let Err(e) = kill(proc) {
325 printer.warning(&format!(
326 "Failed to kill {} [PID {}]: {}",
327 proc.name, proc.pid, e
328 ));
329 failed.push(proc.pid);
330 }
331 }
332 }
333 failed
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use crate::error::ProcError;
340
341 fn this_process() -> Process {
345 Process::find_by_pid(std::process::id())
346 .unwrap()
347 .expect("this process exists")
348 }
349
350 fn refuse(_: &Process) -> Result<()> {
351 Err(ProcError::SignalError("not allowed".to_string()))
352 }
353
354 #[test]
355 fn signals_that_fail_are_reported_not_fatal() {
356 let me = this_process();
357 let printer = Printer::from_flags(true, false);
358 let failed = stop_all(
359 std::slice::from_ref(&me),
360 std::time::Duration::ZERO,
361 &printer,
362 refuse,
363 refuse,
364 );
365 let refusals = if cfg!(unix) { 2 } else { 1 };
368 assert_eq!(failed, vec![me.pid; refusals]);
369 }
370
371 #[test]
372 fn a_refusal_for_a_process_that_is_gone_is_a_warning_and_nothing_is_waited_for() {
373 let mut child = crate::test_support::idle_child();
374 let gone = crate::test_support::gone_record(&mut child);
375 let printer = Printer::from_flags(true, false);
376 let start = std::time::Instant::now();
377 let failed = stop_all(
381 std::slice::from_ref(&gone),
382 std::time::Duration::from_secs(30),
383 &printer,
384 refuse,
385 refuse,
387 );
388 assert_eq!(failed, vec![gone.pid]);
389 assert!(start.elapsed() < std::time::Duration::from_secs(10));
390 }
391
392 #[test]
393 fn signals_that_work_report_nothing() {
394 let me = this_process();
395 let printer = Printer::from_flags(true, false);
396 let failed = stop_all(
397 std::slice::from_ref(&me),
398 std::time::Duration::ZERO,
399 &printer,
400 |_| Ok(()),
401 |_| Ok(()),
402 );
403 assert!(failed.is_empty());
404 }
405}