use std::collections::HashSet;
use std::process::{Command, Stdio};
pub fn port_listeners(port: u16) -> Vec<u32> {
let out = Command::new("lsof")
.args(["-nP", &format!("-iTCP:{port}"), "-sTCP:LISTEN", "-t"])
.stderr(Stdio::null())
.output();
match out {
Ok(o) => String::from_utf8_lossy(&o.stdout)
.lines()
.filter_map(|l| l.trim().parse::<u32>().ok())
.collect(),
Err(_) => Vec::new(),
}
}
pub fn process_name(pid: u32) -> String {
let name = Command::new("ps")
.args(["-o", "comm=", "-p", &pid.to_string()])
.stderr(Stdio::null())
.output()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|s| !s.is_empty())
.map(|s| {
s.rsplit('/').next().unwrap_or(&s).to_string()
});
name.unwrap_or_else(|| format!("pid {pid}"))
}
fn is_descendant_of(pid: u32, ancestor: u32) -> bool {
let mut cur = pid;
for _ in 0..16 {
if cur == ancestor {
return true;
}
if cur <= 1 {
return false;
}
let ppid = Command::new("ps")
.args(["-o", "ppid=", "-p", &cur.to_string()])
.stderr(Stdio::null())
.output()
.ok()
.and_then(|o| {
String::from_utf8_lossy(&o.stdout)
.trim()
.parse::<u32>()
.ok()
});
match ppid {
Some(p) => cur = p,
None => return false,
}
}
false
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PortState {
Free,
OursBound,
Foreign { pid: u32, name: String },
}
pub fn classify_port(port: u16, our_pid: Option<u32>) -> PortState {
let listeners = port_listeners(port);
if listeners.is_empty() {
return PortState::Free;
}
if let Some(mine) = our_pid {
if listeners
.iter()
.any(|&l| l == mine || is_descendant_of(l, mine))
{
return PortState::OursBound;
}
}
let pid = listeners[0];
PortState::Foreign {
pid,
name: process_name(pid),
}
}
pub fn first_foreign(ports: &[u16], our_pid: Option<u32>) -> Option<(u16, u32, String)> {
let mut seen = HashSet::new();
for &port in ports {
if !seen.insert(port) {
continue;
}
if let PortState::Foreign { pid, name } = classify_port(port, our_pid) {
return Some((port, pid, name));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_ports_means_no_conflict() {
assert!(first_foreign(&[], None).is_none());
}
#[test]
fn an_unused_high_port_is_free() {
assert_eq!(classify_port(59_321, None), PortState::Free);
}
}