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