proc_cli/commands/
free.rs1use crate::core::port::PortInfo;
10use crate::core::{parse_target, parse_targets, Process, TargetType};
11use crate::error::{ExitCode, 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_dry_run_with(
107 "free",
108 &processes,
109 &format!(
110 "Dry run: would kill {} process{} to free {} port{}",
111 processes.len(),
112 plural(processes.len()),
113 port_processes.len(),
114 if port_processes.len() == 1 { "" } else { "s" }
115 ),
116 );
117 return Ok(());
118 }
119
120 if !self.yes && !self.json {
121 printer.print_confirmation("free", &processes);
122
123 let prompt = format!(
124 "Kill {} process{} to free {} port{}?",
125 processes.len(),
126 plural(processes.len()),
127 port_processes.len(),
128 if port_processes.len() == 1 { "" } else { "s" }
129 );
130
131 if !Confirm::new()
132 .with_prompt(prompt)
133 .default(false)
134 .interact()?
135 {
136 printer.warning("Aborted");
137 return Ok(());
138 }
139 }
140
141 let mut kill_failed = Vec::new();
143 for proc in &processes {
144 if let Err(e) = proc.terminate() {
145 printer.warning(&format!(
146 "Failed to send SIGTERM to {} [PID {}]: {}",
147 proc.name, proc.pid, e
148 ));
149 kill_failed.push(proc.pid);
150 }
151 }
152
153 let start = std::time::Instant::now();
155 let timeout = std::time::Duration::from_secs(self.wait.min(5));
156 while start.elapsed() < timeout {
157 if processes.iter().all(|p| !p.is_running()) {
158 break;
159 }
160 std::thread::sleep(std::time::Duration::from_millis(100));
161 }
162
163 for proc in &processes {
165 if proc.is_running() {
166 if let Err(e) = proc.kill() {
167 printer.warning(&format!(
168 "Failed to kill {} [PID {}]: {}",
169 proc.name, proc.pid, e
170 ));
171 kill_failed.push(proc.pid);
172 }
173 }
174 }
175
176 let mut freed: Vec<u16> = Vec::new();
178 let mut still_busy: Vec<u16> = Vec::new();
179
180 let poll_start = std::time::Instant::now();
181 let poll_timeout = std::time::Duration::from_secs(self.wait);
182
183 let target_ports: Vec<u16> = port_processes.iter().map(|(port, _)| *port).collect();
184
185 loop {
186 let mut all_free = true;
187 freed.clear();
188 still_busy.clear();
189
190 for port in &target_ports {
191 match PortInfo::find_by_port(*port) {
192 Ok(None) => freed.push(*port),
193 _ => {
194 still_busy.push(*port);
195 all_free = false;
196 }
197 }
198 }
199
200 if all_free || poll_start.elapsed() >= poll_timeout {
201 break;
202 }
203
204 std::thread::sleep(std::time::Duration::from_millis(250));
205 }
206
207 if self.json {
209 let results: Vec<FreeResult> = freed
210 .iter()
211 .map(|p| FreeResult {
212 port: *p,
213 freed: true,
214 })
215 .chain(still_busy.iter().map(|p| FreeResult {
216 port: *p,
217 freed: false,
218 }))
219 .collect();
220
221 printer.print_json(&FreeOutput {
222 action: "free",
223 success: still_busy.is_empty(),
224 results,
225 });
226 } else {
227 for port in &freed {
228 println!(
229 "{} Freed port {}",
230 "✓".green().bold(),
231 port.to_string().cyan().bold()
232 );
233 }
234 for port in &still_busy {
235 println!(
236 "{} Port {} still in use (may be in TIME_WAIT)",
237 "✗".red().bold(),
238 port.to_string().cyan()
239 );
240 }
241 }
242
243 if still_busy.is_empty() {
244 Ok(())
245 } else {
246 Err(ProcError::reported(
247 ExitCode::GeneralError,
248 format!(
249 "{} port{} still in use",
250 still_busy.len(),
251 if still_busy.len() == 1 { "" } else { "s" }
252 ),
253 ))
254 }
255 }
256}
257
258#[derive(Serialize)]
259struct FreeOutput {
260 action: &'static str,
261 success: bool,
262 results: Vec<FreeResult>,
263}
264
265#[derive(Serialize)]
266struct FreeResult {
267 port: u16,
268 freed: bool,
269}