use crate::core::port::PortInfo;
use crate::core::{flatten_targets, parse_target, Process, TargetType};
use crate::error::{ExitCode, ProcError, Result};
use crate::ui::{must_confirm, plural, Printer};
use clap::Args;
use colored::*;
use dialoguer::Confirm;
use serde::Serialize;
#[derive(Args, Debug)]
pub struct FreeCommand {
#[arg(required = true, num_args = 1.., value_parser = crate::core::validate_target_arg)]
pub target: Vec<String>,
#[arg(long, short = 'y')]
pub yes: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(long, short = 'j')]
pub json: bool,
#[arg(long, short = 'v')]
pub verbose: bool,
#[arg(long, default_value = "10")]
pub wait: u64,
}
impl FreeCommand {
pub fn execute(&self) -> Result<()> {
let printer = Printer::from_flags(self.json, self.verbose);
let targets = flatten_targets(&self.target);
let mut ports: Vec<(&String, u16)> = Vec::new();
for target in &targets {
match parse_target(target) {
TargetType::Port(port) => ports.push((target, port)),
_ => {
return Err(ProcError::InvalidInput(format!(
"proc free only works with port targets (e.g. :3000), got '{}'. Use proc kill for processes.",
target
)));
}
}
}
let mut port_processes: Vec<(u16, Process)> = Vec::new();
let mut not_found: Vec<String> = Vec::new();
for (target, port) in &ports {
let mut held = false;
for listener in PortInfo::holders_of(*port)? {
if let Some(proc) = Process::find_by_pid(listener.pid)? {
port_processes.push((*port, proc));
held = true;
}
}
if !held {
printer.warning(&format!("No process listening on port {}", port));
not_found.push((*target).clone());
}
}
let printer = printer.with_not_found(¬_found);
if port_processes.is_empty() {
if not_found.is_empty() {
return Err(ProcError::InvalidInput(
"No port targets specified".to_string(),
));
}
printer.print_empty_result("free", "All specified ports are already free");
return Ok(());
}
let target_ports: Vec<u16> = {
let mut seen = std::collections::HashSet::new();
port_processes
.iter()
.map(|(port, _)| *port)
.filter(|port| seen.insert(*port))
.collect()
};
let mut processes: Vec<Process> = {
let mut seen = std::collections::HashSet::new();
port_processes
.iter()
.filter(|(_, p)| seen.insert(p.pid))
.map(|(_, p)| p.clone())
.collect()
};
if self.dry_run || !self.yes {
Process::measure_cpu(&mut processes);
}
if self.dry_run {
printer.print_dry_run_with(
"free",
&processes,
&format!(
"Dry run: would kill {} process{} to free {} port{}",
processes.len(),
plural(processes.len()),
target_ports.len(),
if target_ports.len() == 1 { "" } else { "s" }
),
);
return Ok(());
}
if must_confirm("free", self.yes, self.json, &processes, Some(¬_found))? {
printer.print_confirmation("free", &processes);
let prompt = format!(
"Kill {} process{} to free {} port{}?",
processes.len(),
plural(processes.len()),
target_ports.len(),
if target_ports.len() == 1 { "" } else { "s" }
);
if !Confirm::new()
.with_prompt(prompt)
.default(false)
.interact()?
{
printer.warning("Aborted");
return Ok(());
}
}
let start = std::time::Instant::now();
stop_all(
&processes,
std::time::Duration::from_secs(self.wait.min(5)),
&printer,
Process::terminate,
Process::kill,
);
let mut freed: Vec<u16> = Vec::new();
let mut still_busy: Vec<u16> = Vec::new();
let poll_deadline = std::cmp::max(
start + std::time::Duration::from_secs(self.wait),
std::time::Instant::now() + std::time::Duration::from_secs(1),
);
let mut was_free = false;
loop {
let mut all_free = true;
freed.clear();
still_busy.clear();
for port in &target_ports {
match PortInfo::holders_of(*port) {
Ok(holders) if holders.is_empty() => freed.push(*port),
_ => {
still_busy.push(*port);
all_free = false;
}
}
}
let settled = all_free && was_free;
was_free = all_free;
if settled || std::time::Instant::now() >= poll_deadline {
break;
}
std::thread::sleep(std::time::Duration::from_millis(250));
}
if self.json {
let results: Vec<FreeResult> = freed
.iter()
.map(|p| FreeResult {
port: *p,
freed: true,
})
.chain(still_busy.iter().map(|p| FreeResult {
port: *p,
freed: false,
}))
.collect();
printer.print_json(&FreeOutput {
action: "free",
success: still_busy.is_empty(),
results,
not_found: ¬_found,
});
} else {
for port in &freed {
println!(
"{} Freed port {}",
"✓".green().bold(),
port.to_string().cyan().bold()
);
}
for port in &still_busy {
println!(
"{} Port {} still in use (may be in TIME_WAIT)",
"✗".red().bold(),
port.to_string().cyan()
);
}
}
if still_busy.is_empty() {
Ok(())
} else {
Err(ProcError::reported(
ExitCode::GeneralError,
format!(
"{} port{} still in use",
still_busy.len(),
if still_busy.len() == 1 { "" } else { "s" }
),
))
}
}
}
#[derive(Serialize)]
struct FreeOutput<'a> {
action: &'static str,
success: bool,
results: Vec<FreeResult>,
not_found: &'a [String],
}
#[derive(Serialize)]
struct FreeResult {
port: u16,
freed: bool,
}
fn stop_all(
processes: &[Process],
grace: std::time::Duration,
printer: &Printer,
terminate: impl Fn(&Process) -> Result<()>,
kill: impl Fn(&Process) -> Result<()>,
) -> Vec<u32> {
let mut failed = Vec::new();
for proc in processes {
match terminate(proc) {
Err(_) if cfg!(not(unix)) && proc.is_running() => {}
Err(e) => {
printer.warning(&format!(
"Failed to ask {} [PID {}] to stop: {}",
proc.name, proc.pid, e
));
failed.push(proc.pid);
}
Ok(()) => {}
}
}
let start = std::time::Instant::now();
while start.elapsed() < grace {
if processes.iter().all(|p| !p.is_running()) {
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
for proc in processes {
if proc.is_running() {
if let Err(e) = kill(proc) {
printer.warning(&format!(
"Failed to kill {} [PID {}]: {}",
proc.name, proc.pid, e
));
failed.push(proc.pid);
}
}
}
failed
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ProcError;
fn this_process() -> Process {
Process::find_by_pid(std::process::id())
.unwrap()
.expect("this process exists")
}
fn refuse(_: &Process) -> Result<()> {
Err(ProcError::SignalError("not allowed".to_string()))
}
#[test]
fn signals_that_fail_are_reported_not_fatal() {
let me = this_process();
let printer = Printer::from_flags(true, false);
let failed = stop_all(
std::slice::from_ref(&me),
std::time::Duration::ZERO,
&printer,
refuse,
refuse,
);
let refusals = if cfg!(unix) { 2 } else { 1 };
assert_eq!(failed, vec![me.pid; refusals]);
}
#[test]
fn a_refusal_for_a_process_that_is_gone_is_a_warning_and_nothing_is_waited_for() {
let mut child = crate::test_support::idle_child();
let gone = crate::test_support::gone_record(&mut child);
let printer = Printer::from_flags(true, false);
let start = std::time::Instant::now();
let failed = stop_all(
std::slice::from_ref(&gone),
std::time::Duration::from_secs(30),
&printer,
refuse,
refuse,
);
assert_eq!(failed, vec![gone.pid]);
assert!(start.elapsed() < std::time::Duration::from_secs(10));
}
#[test]
fn signals_that_work_report_nothing() {
let me = this_process();
let printer = Printer::from_flags(true, false);
let failed = stop_all(
std::slice::from_ref(&me),
std::time::Duration::ZERO,
&printer,
|_| Ok(()),
|_| Ok(()),
);
assert!(failed.is_empty());
}
}