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::{OutputFormat, 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 format = if self.json {
50 OutputFormat::Json
51 } else {
52 OutputFormat::Human
53 };
54 let printer = Printer::new(format, self.verbose);
55
56 let targets = parse_targets(&self.target);
57
58 let mut ports: Vec<u16> = Vec::new();
60 for target in &targets {
61 match parse_target(target) {
62 TargetType::Port(port) => ports.push(port),
63 _ => {
64 return Err(ProcError::InvalidInput(format!(
65 "proc free only works with port targets (e.g. :3000), got '{}'. Use proc kill for processes.",
66 target
67 )));
68 }
69 }
70 }
71
72 let mut port_processes: Vec<(u16, Process)> = Vec::new();
74 let mut not_found: Vec<u16> = Vec::new();
75
76 for port in &ports {
77 match PortInfo::find_by_port(*port)? {
78 Some(port_info) => match Process::find_by_pid(port_info.pid)? {
79 Some(proc) => port_processes.push((*port, proc)),
80 None => not_found.push(*port),
81 },
82 None => not_found.push(*port),
83 }
84 }
85
86 for port in ¬_found {
87 printer.warning(&format!("No process listening on port {}", port));
88 }
89
90 if port_processes.is_empty() {
91 if not_found.is_empty() {
92 return Err(ProcError::InvalidInput(
93 "No port targets specified".to_string(),
94 ));
95 }
96 printer.success("All specified ports are already free");
97 return Ok(());
98 }
99
100 let processes: Vec<Process> = {
102 let mut seen = std::collections::HashSet::new();
103 port_processes
104 .iter()
105 .filter(|(_, p)| seen.insert(p.pid))
106 .map(|(_, p)| p.clone())
107 .collect()
108 };
109
110 if self.dry_run {
111 printer.print_processes(&processes);
112 printer.warning(&format!(
113 "Dry run: would kill {} process{} to free {} port{}",
114 processes.len(),
115 if processes.len() == 1 { "" } else { "es" },
116 port_processes.len(),
117 if port_processes.len() == 1 { "" } else { "s" }
118 ));
119 return Ok(());
120 }
121
122 if !self.yes && !self.json {
123 printer.print_confirmation("free", &processes);
124
125 let prompt = format!(
126 "Kill {} process{} to free {} port{}?",
127 processes.len(),
128 if processes.len() == 1 { "" } else { "es" },
129 port_processes.len(),
130 if port_processes.len() == 1 { "" } else { "s" }
131 );
132
133 if !Confirm::new()
134 .with_prompt(prompt)
135 .default(false)
136 .interact()?
137 {
138 printer.warning("Aborted");
139 return Ok(());
140 }
141 }
142
143 let mut kill_failed = Vec::new();
145 for proc in &processes {
146 if let Err(e) = proc.terminate() {
147 printer.warning(&format!(
148 "Failed to send SIGTERM to {} [PID {}]: {}",
149 proc.name, proc.pid, e
150 ));
151 kill_failed.push(proc.pid);
152 }
153 }
154
155 let start = std::time::Instant::now();
157 let timeout = std::time::Duration::from_secs(self.wait.min(5));
158 while start.elapsed() < timeout {
159 if processes.iter().all(|p| !p.is_running()) {
160 break;
161 }
162 std::thread::sleep(std::time::Duration::from_millis(100));
163 }
164
165 for proc in &processes {
167 if proc.is_running() {
168 if let Err(e) = proc.kill() {
169 printer.warning(&format!(
170 "Failed to kill {} [PID {}]: {}",
171 proc.name, proc.pid, e
172 ));
173 kill_failed.push(proc.pid);
174 }
175 }
176 }
177
178 let mut freed: Vec<u16> = Vec::new();
180 let mut still_busy: Vec<u16> = Vec::new();
181
182 let poll_start = std::time::Instant::now();
183 let poll_timeout = std::time::Duration::from_secs(self.wait);
184
185 let target_ports: Vec<u16> = port_processes.iter().map(|(port, _)| *port).collect();
186
187 loop {
188 let mut all_free = true;
189 freed.clear();
190 still_busy.clear();
191
192 for port in &target_ports {
193 match PortInfo::find_by_port(*port) {
194 Ok(None) => freed.push(*port),
195 _ => {
196 still_busy.push(*port);
197 all_free = false;
198 }
199 }
200 }
201
202 if all_free || poll_start.elapsed() >= poll_timeout {
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 });
228 } else {
229 for port in &freed {
230 println!(
231 "{} Freed port {}",
232 "✓".green().bold(),
233 port.to_string().cyan().bold()
234 );
235 }
236 for port in &still_busy {
237 println!(
238 "{} Port {} still in use (may be in TIME_WAIT)",
239 "✗".red().bold(),
240 port.to_string().cyan()
241 );
242 }
243 }
244
245 Ok(())
246 }
247}
248
249#[derive(Serialize)]
250struct FreeOutput {
251 action: &'static str,
252 success: bool,
253 results: Vec<FreeResult>,
254}
255
256#[derive(Serialize)]
257struct FreeResult {
258 port: u16,
259 freed: bool,
260}