1use crate::core::port::PortInfo;
12use crate::core::{flatten_targets, parse_target, Process, TargetType};
13use crate::error::{ExitCode, ProcError, Result};
14use crate::out_println;
15use crate::ui::{must_confirm, plural, prompt, Printer};
16use clap::Args;
17use colored::*;
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() {
95 printer.print_empty_result("free", "All specified ports are already free");
96 return Ok(());
97 }
98
99 let target_ports: Vec<u16> = {
101 let mut seen = std::collections::HashSet::new();
102 port_processes
103 .iter()
104 .map(|(port, _)| *port)
105 .filter(|port| seen.insert(*port))
106 .collect()
107 };
108
109 let mut processes: Vec<Process> = {
111 let mut seen = std::collections::HashSet::new();
112 port_processes
113 .iter()
114 .filter(|(_, p)| seen.insert(p.pid))
115 .map(|(_, p)| p.clone())
116 .collect()
117 };
118
119 if self.dry_run || !self.yes {
122 Process::measure_cpu(&mut processes);
123 }
124
125 if self.dry_run {
126 printer.print_dry_run_with(
127 "free",
128 &processes,
129 &format!(
130 "Dry run: would kill {} process{} to free {} port{}",
131 processes.len(),
132 plural(processes.len()),
133 target_ports.len(),
134 if target_ports.len() == 1 { "" } else { "s" }
135 ),
136 );
137 return Ok(());
138 }
139
140 if must_confirm("free", self.yes, self.json, &processes, Some(¬_found))? {
142 let question = format!(
143 "Kill {} process{} to free {} port{}?",
144 processes.len(),
145 plural(processes.len()),
146 target_ports.len(),
147 if target_ports.len() == 1 { "" } else { "s" }
148 );
149
150 if !prompt::ask("free", &processes, Some(¬_found), &question, || {
151 printer.print_confirmation("free", &processes)
152 })? {
153 printer.warning("Aborted");
154 return Ok(());
155 }
156 }
157
158 let start = std::time::Instant::now();
160 stop_all(
161 &processes,
162 std::time::Duration::from_secs(self.wait.min(5)),
163 &printer,
164 Process::terminate,
165 Process::kill,
166 );
167
168 let mut freed: Vec<u16> = Vec::new();
170 let mut still_busy: Vec<u16> = Vec::new();
171
172 let poll_deadline = std::cmp::max(
175 start + std::time::Duration::from_secs(self.wait),
176 std::time::Instant::now() + std::time::Duration::from_secs(1),
177 );
178
179 let mut was_free = false;
184 loop {
185 let mut all_free = true;
186 freed.clear();
187 still_busy.clear();
188
189 for port in &target_ports {
190 match PortInfo::holders_of(*port) {
192 Ok(holders) if holders.is_empty() => freed.push(*port),
193 _ => {
194 still_busy.push(*port);
195 all_free = false;
196 }
197 }
198 }
199
200 let settled = all_free && was_free;
201 was_free = all_free;
202 if settled || std::time::Instant::now() >= poll_deadline {
203 break;
204 }
205
206 std::thread::sleep(std::time::Duration::from_millis(250));
207 }
208
209 if self.json {
211 let results: Vec<FreeResult> = freed
212 .iter()
213 .map(|p| FreeResult {
214 port: *p,
215 freed: true,
216 })
217 .chain(still_busy.iter().map(|p| FreeResult {
218 port: *p,
219 freed: false,
220 }))
221 .collect();
222
223 printer.print_json(&FreeOutput {
224 action: "free",
225 success: still_busy.is_empty(),
226 results,
227 not_found: ¬_found,
228 });
229 } else {
230 for port in &freed {
231 out_println!(
232 "{} Freed port {}",
233 "✓".green().bold(),
234 port.to_string().cyan().bold()
235 );
236 }
237 for port in &still_busy {
238 out_println!(
239 "{} Port {} still in use (may be in TIME_WAIT)",
240 "✗".red().bold(),
241 port.to_string().cyan()
242 );
243 }
244 }
245
246 if still_busy.is_empty() {
247 Ok(())
248 } else {
249 Err(ProcError::reported(
250 ExitCode::GeneralError,
251 format!(
252 "{} port{} still in use",
253 still_busy.len(),
254 if still_busy.len() == 1 { "" } else { "s" }
255 ),
256 ))
257 }
258 }
259}
260
261#[derive(Serialize)]
262struct FreeOutput<'a> {
263 action: &'static str,
264 success: bool,
265 results: Vec<FreeResult>,
266 not_found: &'a [String],
268}
269
270#[derive(Serialize)]
271struct FreeResult {
272 port: u16,
273 freed: bool,
274}
275
276fn stop_all(
283 processes: &[Process],
284 grace: std::time::Duration,
285 printer: &Printer,
286 terminate: impl Fn(&Process) -> Result<()>,
287 kill: impl Fn(&Process) -> Result<()>,
288) -> Vec<u32> {
289 let mut failed = Vec::new();
290 for proc in processes {
291 match terminate(proc) {
292 Err(_) if cfg!(not(unix)) && proc.is_running() => {}
295 Err(e) => {
296 printer.warning(&format!(
297 "Failed to ask {} [PID {}] to stop: {}",
298 proc.name, proc.pid, e
299 ));
300 failed.push(proc.pid);
301 }
302 Ok(()) => {}
303 }
304 }
305
306 let start = std::time::Instant::now();
308 while start.elapsed() < grace {
309 if processes.iter().all(|p| !p.is_running()) {
310 break;
311 }
312 std::thread::sleep(std::time::Duration::from_millis(100));
313 }
314
315 for proc in processes {
317 if proc.is_running() {
318 if let Err(e) = kill(proc) {
319 printer.warning(&format!(
320 "Failed to kill {} [PID {}]: {}",
321 proc.name, proc.pid, e
322 ));
323 failed.push(proc.pid);
324 }
325 }
326 }
327 failed
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::error::ProcError;
334
335 fn this_process() -> Process {
339 Process::find_by_pid(std::process::id())
340 .unwrap()
341 .expect("this process exists")
342 }
343
344 fn refuse(_: &Process) -> Result<()> {
345 Err(ProcError::SignalError("not allowed".to_string()))
346 }
347
348 #[test]
349 fn signals_that_fail_are_reported_not_fatal() {
350 let me = this_process();
351 let printer = Printer::from_flags(true, false);
352 let failed = stop_all(
353 std::slice::from_ref(&me),
354 std::time::Duration::ZERO,
355 &printer,
356 refuse,
357 refuse,
358 );
359 let refusals = if cfg!(unix) { 2 } else { 1 };
362 assert_eq!(failed, vec![me.pid; refusals]);
363 }
364
365 #[test]
366 fn a_refusal_for_a_process_that_is_gone_is_a_warning_and_nothing_is_waited_for() {
367 let mut child = crate::test_support::idle_child();
368 let gone = crate::test_support::gone_record(&mut child);
369 let printer = Printer::from_flags(true, false);
370 let start = std::time::Instant::now();
371 let failed = stop_all(
375 std::slice::from_ref(&gone),
376 std::time::Duration::from_secs(30),
377 &printer,
378 refuse,
379 refuse,
381 );
382 assert_eq!(failed, vec![gone.pid]);
383 assert!(start.elapsed() < std::time::Duration::from_secs(10));
384 }
385
386 #[test]
387 fn signals_that_work_report_nothing() {
388 let me = this_process();
389 let printer = Printer::from_flags(true, false);
390 let failed = stop_all(
391 std::slice::from_ref(&me),
392 std::time::Duration::ZERO,
393 &printer,
394 |_| Ok(()),
395 |_| Ok(()),
396 );
397 assert!(failed.is_empty());
398 }
399}