# single port
$ port-finder 3000 # several
$ port-finder 3000 8080 5432 # range
$ port-finder 3000-3100 # show everything
$ port-finder # kill after showing
$ port-finder 3000 --kill # SIGTERM
$ port-finder 3000 --force # SIGKILL # into a pipeline
$ port-finder --json 8080 | jq '.listeners[] | {pid, command}'
# single port
$ port-finder 3000 # several
$ port-finder 3000 8080 5432 # range
$ port-finder 3000-3100 # show everything
$ port-finder # kill after showing
$ port-finder 3000 --kill # SIGTERM
$ port-finder 3000 --force # SIGKILL # into a pipeline
$ port-finder --json 8080 | jq '.listeners[] | {pid, command}'
# single port
$ port-finder 3000 # several
$ port-finder 3000 8080 5432 # range
$ port-finder 3000-3100 # show everything
$ port-finder # kill after showing
$ port-finder 3000 --kill # SIGTERM
$ port-finder 3000 --force # SIGKILL # into a pipeline
$ port-finder --json 8080 | jq '.listeners[] | {pid, command}'
$ lsof -nP -iTCP -sTCP:LISTEN -F pcLnt
p1103
crapportd
Lme
f12
tIPv4
n*:57768
f14
tIPv6
n*:57768
$ lsof -nP -iTCP -sTCP:LISTEN -F pcLnt
p1103
crapportd
Lme
f12
tIPv4
n*:57768
f14
tIPv6
n*:57768
$ lsof -nP -iTCP -sTCP:LISTEN -F pcLnt
p1103
crapportd
Lme
f12
tIPv4
n*:57768
f14
tIPv6
n*:57768
for raw in input.lines() { let (tag, value) = raw.split_at(1); match tag { "p" => { pid = Some(value.parse()?); command = None; user = None; ipv6 = false; } "c" => command = Some(value.into()), "L" => user = Some(value.into()), "f" => ipv6 = false, // new fd → family unknown until `t` arrives "t" => ipv6 = value == "IPv6", "n" => { /* split `addr:port`, push a Listener */ } _ => {} }
}
for raw in input.lines() { let (tag, value) = raw.split_at(1); match tag { "p" => { pid = Some(value.parse()?); command = None; user = None; ipv6 = false; } "c" => command = Some(value.into()), "L" => user = Some(value.into()), "f" => ipv6 = false, // new fd → family unknown until `t` arrives "t" => ipv6 = value == "IPv6", "n" => { /* split `addr:port`, push a Listener */ } _ => {} }
}
for raw in input.lines() { let (tag, value) = raw.split_at(1); match tag { "p" => { pid = Some(value.parse()?); command = None; user = None; ipv6 = false; } "c" => command = Some(value.into()), "L" => user = Some(value.into()), "f" => ipv6 = false, // new fd → family unknown until `t` arrives "t" => ipv6 = value == "IPv6", "n" => { /* split `addr:port`, push a Listener */ } _ => {} }
}
let address = match addr { "*" if ipv6 => "[::]".to_string(), "*" => "0.0.0.0".to_string(), other => other.to_string(),
};
let address = match addr { "*" if ipv6 => "[::]".to_string(), "*" => "0.0.0.0".to_string(), other => other.to_string(),
};
let address = match addr { "*" if ipv6 => "[::]".to_string(), "*" => "0.0.0.0".to_string(), other => other.to_string(),
};
sl local_address rem_address st ... uid ... inode 0: 00000000:0BB8 00000000:0000 0A ... 1000 ... 987654 ...
sl local_address rem_address st ... uid ... inode 0: 00000000:0BB8 00000000:0000 0A ... 1000 ... 987654 ...
sl local_address rem_address st ... uid ... inode 0: 00000000:0BB8 00000000:0000 0A ... 1000 ... 987654 ...
let word = u32::from_str_radix(addr_hex, 16)?;
Ipv4Addr::from(word.to_le_bytes()) // not Ipv4Addr::from(word)
let word = u32::from_str_radix(addr_hex, 16)?;
Ipv4Addr::from(word.to_le_bytes()) // not Ipv4Addr::from(word)
let word = u32::from_str_radix(addr_hex, 16)?;
Ipv4Addr::from(word.to_le_bytes()) // not Ipv4Addr::from(word)
let port = u16::from_str_radix(port_hex, 16)?; // just parse
let port = u16::from_str_radix(port_hex, 16)?; // just parse
let port = u16::from_str_radix(port_hex, 16)?; // just parse
fn scan_proc_sockets() -> HashMap<u64, u32> { let mut map = HashMap::new(); for entry in std::fs::read_dir("/proc")?.flatten() { let Ok(pid) = entry.file_name().to_string_lossy().parse::<u32>() else { continue }; let Ok(fds) = std::fs::read_dir(format!("/proc/{pid}/fd")) else { continue }; for fd in fds.flatten() { let Ok(link) = std::fs::read_link(fd.path()) else { continue }; if let Some(inode) = extract_socket_inode(&link.to_string_lossy()) { map.entry(inode).or_insert(pid); } } } map
}
fn scan_proc_sockets() -> HashMap<u64, u32> { let mut map = HashMap::new(); for entry in std::fs::read_dir("/proc")?.flatten() { let Ok(pid) = entry.file_name().to_string_lossy().parse::<u32>() else { continue }; let Ok(fds) = std::fs::read_dir(format!("/proc/{pid}/fd")) else { continue }; for fd in fds.flatten() { let Ok(link) = std::fs::read_link(fd.path()) else { continue }; if let Some(inode) = extract_socket_inode(&link.to_string_lossy()) { map.entry(inode).or_insert(pid); } } } map
}
fn scan_proc_sockets() -> HashMap<u64, u32> { let mut map = HashMap::new(); for entry in std::fs::read_dir("/proc")?.flatten() { let Ok(pid) = entry.file_name().to_string_lossy().parse::<u32>() else { continue }; let Ok(fds) = std::fs::read_dir(format!("/proc/{pid}/fd")) else { continue }; for fd in fds.flatten() { let Ok(link) = std::fs::read_link(fd.path()) else { continue }; if let Some(inode) = extract_socket_inode(&link.to_string_lossy()) { map.entry(inode).or_insert(pid); } } } map
}
> netstat -ano -p TCP TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 964 TCP [::]:3000 [::]:0 LISTENING 12345
> netstat -ano -p TCP TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 964 TCP [::]:3000 [::]:0 LISTENING 12345
> netstat -ano -p TCP TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 964 TCP [::]:3000 [::]:0 LISTENING 12345
pub fn split_address(s: &str) -> Option<(&str, &str)> { let mut depth = 0i32; let mut last = None; for (i, ch) in s.char_indices() { match ch { '[' => depth += 1, ']' => depth -= 1, ':' if depth == 0 => last = Some(i), _ => {} } } let i = last?; Some((&s[..i], &s[i + 1..]))
}
pub fn split_address(s: &str) -> Option<(&str, &str)> { let mut depth = 0i32; let mut last = None; for (i, ch) in s.char_indices() { match ch { '[' => depth += 1, ']' => depth -= 1, ':' if depth == 0 => last = Some(i), _ => {} } } let i = last?; Some((&s[..i], &s[i + 1..]))
}
pub fn split_address(s: &str) -> Option<(&str, &str)> { let mut depth = 0i32; let mut last = None; for (i, ch) in s.char_indices() { match ch { '[' => depth += 1, ']' => depth -= 1, ':' if depth == 0 => last = Some(i), _ => {} } } let i = last?; Some((&s[..i], &s[i + 1..]))
}
> tasklist /FO CSV /NH
"System","4","Services","0","136 K"
"node.exe","12345","Console","1","100,032 K"
> tasklist /FO CSV /NH
"System","4","Services","0","136 K"
"node.exe","12345","Console","1","100,032 K"
> tasklist /FO CSV /NH
"System","4","Services","0","136 K"
"node.exe","12345","Console","1","100,032 K"
fn split_csv_row(line: &str) -> Vec<String> { let mut out = Vec::new(); let mut cur = String::new(); let mut in_quotes = false; let mut chars = line.chars().peekable(); while let Some(ch) = chars.next() { match ch { '"' => { if in_quotes && chars.peek() == Some(&'"') { cur.push('"'); chars.next(); // `""` inside a quoted field is a literal quote } else { in_quotes = !in_quotes; } } ',' if !in_quotes => out.push(std::mem::take(&mut cur)), _ => cur.push(ch), } } out.push(cur); out
}
fn split_csv_row(line: &str) -> Vec<String> { let mut out = Vec::new(); let mut cur = String::new(); let mut in_quotes = false; let mut chars = line.chars().peekable(); while let Some(ch) = chars.next() { match ch { '"' => { if in_quotes && chars.peek() == Some(&'"') { cur.push('"'); chars.next(); // `""` inside a quoted field is a literal quote } else { in_quotes = !in_quotes; } } ',' if !in_quotes => out.push(std::mem::take(&mut cur)), _ => cur.push(ch), } } out.push(cur); out
}
fn split_csv_row(line: &str) -> Vec<String> { let mut out = Vec::new(); let mut cur = String::new(); let mut in_quotes = false; let mut chars = line.chars().peekable(); while let Some(ch) = chars.next() { match ch { '"' => { if in_quotes && chars.peek() == Some(&'"') { cur.push('"'); chars.next(); // `""` inside a quoted field is a literal quote } else { in_quotes = !in_quotes; } } ',' if !in_quotes => out.push(std::mem::take(&mut cur)), _ => cur.push(ch), } } out.push(cur); out
}
// src/linux.rs
pub fn parse(tcp: &str, tcp6: &str, ports: &[u16]) -> Result<Vec<TcpEntry>, Error> { ... } #[cfg(target_os = "linux")]
pub fn find(ports: &[u16]) -> Result<Vec<Listener>, Error> { let tcp = std::fs::read_to_string("/proc/net/tcp").unwrap_or_default(); let tcp6 = std::fs::read_to_string("/proc/net/tcp6").unwrap_or_default(); parse(&tcp, &tcp6, ports).map(/* + inode → pid resolution */)
} #[cfg(not(target_os = "linux"))]
pub fn find(_: &[u16]) -> Result<Vec<Listener>, Error> { Err(Error::Unsupported) }
// src/linux.rs
pub fn parse(tcp: &str, tcp6: &str, ports: &[u16]) -> Result<Vec<TcpEntry>, Error> { ... } #[cfg(target_os = "linux")]
pub fn find(ports: &[u16]) -> Result<Vec<Listener>, Error> { let tcp = std::fs::read_to_string("/proc/net/tcp").unwrap_or_default(); let tcp6 = std::fs::read_to_string("/proc/net/tcp6").unwrap_or_default(); parse(&tcp, &tcp6, ports).map(/* + inode → pid resolution */)
} #[cfg(not(target_os = "linux"))]
pub fn find(_: &[u16]) -> Result<Vec<Listener>, Error> { Err(Error::Unsupported) }
// src/linux.rs
pub fn parse(tcp: &str, tcp6: &str, ports: &[u16]) -> Result<Vec<TcpEntry>, Error> { ... } #[cfg(target_os = "linux")]
pub fn find(ports: &[u16]) -> Result<Vec<Listener>, Error> { let tcp = std::fs::read_to_string("/proc/net/tcp").unwrap_or_default(); let tcp6 = std::fs::read_to_string("/proc/net/tcp6").unwrap_or_default(); parse(&tcp, &tcp6, ports).map(/* + inode → pid resolution */)
} #[cfg(not(target_os = "linux"))]
pub fn find(_: &[u16]) -> Result<Vec<Listener>, Error> { Err(Error::Unsupported) }
const TCP4: &str = "\ 0: 00000000:0BB8 00000000:0000 0A ... 1000 ... 987654 ... 1: 0100007F:1F90 0100007F:C442 01 ... 0 ... 111111 ... // ESTABLISHED — skipped 2: 0100007F:0050 00000000:0000 0A ... 0 ... 222222 ...
"; #[test]
fn listen_rows_only() { let got = parse(TCP4, "", &[]).unwrap(); assert_eq!(got.len(), 2); // the ESTABLISHED row at index 1 is excluded assert_eq!(got[0].port, 3000); assert_eq!(got[0].address, "0.0.0.0");
}
const TCP4: &str = "\ 0: 00000000:0BB8 00000000:0000 0A ... 1000 ... 987654 ... 1: 0100007F:1F90 0100007F:C442 01 ... 0 ... 111111 ... // ESTABLISHED — skipped 2: 0100007F:0050 00000000:0000 0A ... 0 ... 222222 ...
"; #[test]
fn listen_rows_only() { let got = parse(TCP4, "", &[]).unwrap(); assert_eq!(got.len(), 2); // the ESTABLISHED row at index 1 is excluded assert_eq!(got[0].port, 3000); assert_eq!(got[0].address, "0.0.0.0");
}
const TCP4: &str = "\ 0: 00000000:0BB8 00000000:0000 0A ... 1000 ... 987654 ... 1: 0100007F:1F90 0100007F:C442 01 ... 0 ... 111111 ... // ESTABLISHED — skipped 2: 0100007F:0050 00000000:0000 0A ... 0 ... 222222 ...
"; #[test]
fn listen_rows_only() { let got = parse(TCP4, "", &[]).unwrap(); assert_eq!(got.len(), 2); // the ESTABLISHED row at index 1 is excluded assert_eq!(got[0].port, 3000); assert_eq!(got[0].address, "0.0.0.0");
}
pub fn kill_pid(pid: u32, force: bool) -> Result<(), Error> { #[cfg(unix)] { let sig = if force { "-9" } else { "-15" }; Command::new("kill").arg(sig).arg(pid.to_string()).-weight: 500;">status()?; } #[cfg(windows)] { let mut cmd = Command::new("taskkill"); cmd.arg("/PID").arg(pid.to_string()); if force { cmd.arg("/F"); } cmd.-weight: 500;">status()?; } Ok(())
}
pub fn kill_pid(pid: u32, force: bool) -> Result<(), Error> { #[cfg(unix)] { let sig = if force { "-9" } else { "-15" }; Command::new("kill").arg(sig).arg(pid.to_string()).-weight: 500;">status()?; } #[cfg(windows)] { let mut cmd = Command::new("taskkill"); cmd.arg("/PID").arg(pid.to_string()); if force { cmd.arg("/F"); } cmd.-weight: 500;">status()?; } Ok(())
}
pub fn kill_pid(pid: u32, force: bool) -> Result<(), Error> { #[cfg(unix)] { let sig = if force { "-9" } else { "-15" }; Command::new("kill").arg(sig).arg(pid.to_string()).-weight: 500;">status()?; } #[cfg(windows)] { let mut cmd = Command::new("taskkill"); cmd.arg("/PID").arg(pid.to_string()); if force { cmd.arg("/F"); } cmd.-weight: 500;">status()?; } Ok(())
}
test result: ok. 31 passed (lib)
test result: ok. 5 passed (main)
test result: ok. 6 passed (cli integration)
test result: ok. 31 passed (lib)
test result: ok. 5 passed (main)
test result: ok. 6 passed (cli integration)
test result: ok. 31 passed (lib)
test result: ok. 5 passed (main)
test result: ok. 6 passed (cli integration)
[profile.release]
strip = true
lto = true
codegen-units = 1
opt-level = "z"
panic = "abort"
[profile.release]
strip = true
lto = true
codegen-units = 1
opt-level = "z"
panic = "abort"
[profile.release]
strip = true
lto = true
codegen-units = 1
opt-level = "z"
panic = "abort" - Same command on every OS. port-finder 3000 works on macOS, Linux and Windows.
- Same output shape. Fixed five columns: PORT / PID / COMMAND / USER / ADDRESS.
- Don't lie about dual-stack. A process bound to both 0.0.0.0:3000 and [::]:3000 is two sockets, not one — show both rows.
- One-shot kill. --kill (SIGTERM) or --force (SIGKILL) without pulling out a second command.
- --json for scripts. Anything I build has a monitoring / automation path.
- cargo -weight: 500;">install-able static binary. No C dependency, no OpenSSL. - u32::from_str_radix("0100007F", 16) → 0x0100007F (host-order u32)
- .to_le_bytes() → [0x7F, 0x00, 0x00, 0x01] ← the original network-order bytes