Skip to main content

proc_cli/commands/
free.rs

1//! `proc free` - Free ports by killing their processes and verifying availability
2//!
3//! Examples:
4//!   proc free :3000              # Kill process on port 3000, verify port freed
5//!   proc free :3000,:8080        # Free multiple ports
6//!   proc free :3000 --yes        # Skip confirmation
7//!   proc free :3000 --wait 30    # Wait up to 30s for port to free
8
9use 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/// Free port(s) by killing the process and verifying the port is available
19#[derive(Args, Debug)]
20pub struct FreeCommand {
21    /// Port target(s): :port (comma-separated for multiple, e.g. :3000,:8080)
22    #[arg(required = true)]
23    pub target: String,
24
25    /// Skip confirmation prompt
26    #[arg(long, short = 'y')]
27    pub yes: bool,
28
29    /// Show what would be freed without actually freeing
30    #[arg(long)]
31    pub dry_run: bool,
32
33    /// Output as JSON
34    #[arg(long, short = 'j')]
35    pub json: bool,
36
37    /// Show verbose output
38    #[arg(long, short = 'v')]
39    pub verbose: bool,
40
41    /// Max seconds to wait for port to become free
42    #[arg(long, default_value = "10")]
43    pub wait: u64,
44}
45
46impl FreeCommand {
47    /// Executes the free command, killing processes and verifying ports are freed.
48    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        // Validate all targets are ports
59        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        // Resolve ports to processes
73        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 &not_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        // Deduplicate processes (multiple ports might be held by same process)
101        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        // Terminate processes (SIGTERM first)
144        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        // Wait for graceful exit
156        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        // Force kill any remaining
166        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        // Poll until ports are free
179        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        // Report results
210        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}