Skip to content

Latest commit

 

History

History
216 lines (155 loc) · 8.74 KB

File metadata and controls

216 lines (155 loc) · 8.74 KB

whoseport

Whose port is it anyway?

Python · zero dependencies · Windows / macOS / Linux.

One command to see which process is squatting on a port — and free it if you want. Pure Python standard library: no psutil, no compiled wheels, no Node, no Rust toolchain.

CI Release Python License: MIT OS

English | 简体中文

whoseport demo: see who is on port 8080, then free it

The problem

Address already in use. EADDRINUSE. bind: Only one usage of each socket address.

You know the drill — and you have googled this exact incantation more times than you'd like to admit:

Step Windows macOS / Linux
1. find the PID netstat -ano | findstr :8080 lsof -i :8080 (was it -i:8080? -nP?)
2. find out what it is tasklist | findstr 23456 ps -p 23456
3. kill it taskkill /F /PID 23456 kill -9 23456

Three commands, two syntaxes to remember, and on non-English Windows the netstat output is localized so half the copy-pasted one-liners break anyway.

The fix

$ whoseport 8080
PORT  PROTO  STATE     PID  PROCESS  ADDRESS
8080  tcp    LISTEN  31425  node     0.0.0.0
     └─ node server.js --port 8080

$ whoseport 8080 --kill
About to terminate 1 process:
  PID 31425  node  (port 8080)
Proceed? [y/N] y
✓ PID 31425 (node): SIGTERM sent
✓ port 8080: free

That's it. It shows you who is on the port before anything gets killed.

Install

Python 3.9+, zero dependencies. Not on PyPI yet:

pip install git+https://github.com/hc-ui/whoseport.git

Same toolbox: wholocks · longpath · eoldoctor · whichproxy

Usage

$ whoseport                  # what's listening on this machine?
$ whoseport 8080             # who is using port 8080?
$ whoseport 3000-3010 5432   # multiple ports and ranges
$ whoseport 8080 --kill      # terminate the owner (asks first)
$ whoseport 8080 -k -y -f    # no questions, SIGKILL (CI-friendly)
$ whoseport 8080 --wait 60   # block until the port is free (CI, restart loops)
$ whoseport 8080 --json      # machine-readable, for scripts and AI agents
$ whoseport 8080 --tcp       # TCP only (--udp for UDP only)
$ whoseport 8080 --all       # every socket, incl. established connections

Busy ports stay readable — connection noise is collapsed, listeners stay front and center:

$ whoseport 7897
PORT  PROTO  STATE     PID  PROCESS           ADDRESS
7897  tcp    LISTEN  34040  verge-mihomo.exe  0.0.0.0
7897  tcp6   LISTEN  34040  verge-mihomo.exe  [::]
7897  udp    -       34040  verge-mihomo.exe  0.0.0.0
  + 165 ESTABLISHED, 77 TIME_WAIT connection(s) from 1 process(es) on the same port(s) - use --all to list them

Exit codes

Code Meaning
0 found something (or killed it successfully)
1 nothing is using the port(s)
2 usage / platform error
3 kill failed or the port is still busy

So whoseport 8080 || echo free just works in scripts.

Why not ...?

whoseport killport (Rust) kill-port (npm) psutil scripts raw netstat / lsof
Install pip / pipx / uvx brew / cargo needs Node needs a compiled dep built-in
Runtime deps none (pure stdlib) none (binary) Node runtime psutil wheel —
Shows who before killing ✓ table + cmdline dry-run flag partial write it yourself arcane flags
Safe by default ✓ confirm + SIGTERM first ✗ SIGKILL by default ✗ kills immediately up to you manual
Explains TIME_WAIT ✓ ✗ ✗ ✗ raw rows
Locale-proof on Windows ✓ native API ✓ ✗ parses netstat text ✓ ✗ (LISTENING vs 侦听)
JSON output ✓ ✗ ✗ write it yourself ✗
Verifies the port is actually free after killing ✓ ✗ ✗ write it yourself manual

If you live in the Rust or Node ecosystem, those tools are great. whoseport is for everyone who already has Python — which, if you fight with ports 8000/8888/5000, you probably do.

How it works

No psutil. Each platform gets the same treatment the native tools use, minus the parsing hazards:

  • Linux — reads /proc/net/{tcp,tcp6,udp,udp6} and maps socket inodes to PIDs via /proc/*/fd (exactly what ss -p does). No subprocess at all.
  • Windows — calls GetExtendedTcpTable / GetExtendedUdpTable from iphlpapi.dll via ctypes — the same API netstat itself uses. Locale-proof by construction, no console codepage headaches. Elevated process names are recovered through one batched tasklist call.
  • macOS — uses the lsof that ships with macOS, in -F machine-readable field mode (not the human table, which shifts between versions).

Safety

--kill is designed to be hard to regret:

  • Shows exactly what will die and asks for confirmation (skip with --yes).
  • Sends SIGTERM first; SIGKILL only with --force. (On Windows, TerminateProcess is the only reliable option for console apps — equivalent to taskkill /F — and the docs say so instead of pretending otherwise.)
  • Refuses to touch PID 0/1/4, itself, or its parent shell.
  • Re-checks the port afterwards and tells you whether it is actually free, including lingering TIME_WAIT sockets that clear on their own.

Scripting & AI agents

--json emits a stable document — handy for shell scripts and for LLM agents that keep hitting EADDRINUSE in dev loops:

{
  "whoseport": "0.1.0",
  "platform": "win32",
  "query": [8080],
  "sockets": [
    {
      "proto": "tcp", "ip_version": 4, "local_addr": "0.0.0.0",
      "local_port": 8080, "state": "LISTEN", "pid": 31425,
      "process": "node", "cmdline": "node server.js --port 8080"
    }
  ]
}
# block until port 8080 is free (exit 0), or give up after 60s (exit 3)
whoseport 8080 --wait 60

# free a port in CI before starting the dev server
whoseport 3000 --kill --yes --force || true

# one-liner check: is the port free right now?
whoseport 8080 >/dev/null || echo "free"   # exit 1 = free

Use as a library

The same engine is importable — still zero dependencies:

import whoseport

for sock in whoseport.collect(ports={8080}):
    print(sock.pid, sock.process, sock.state, sock.local_addr)

FAQ

Some rows show ? as PID on Linux. Sockets of other users are visible, but their owners aren't (same as netstat). Run with sudo to see everything.

A TIME_WAIT row is shown with - as the process. That socket has no live owner — the kernel keeps it briefly after a connection closes. It clears on its own, and servers that set SO_REUSEADDR can bind right through it. whoseport tells you this instead of letting you hunt for a process that doesn't exist.

On Windows the process name shows but the path doesn't. The owner runs elevated. Names come from tasklist (works from any terminal); full paths need an elevated terminal.

The port is owned by docker-proxy / com.docker.backend / vpnkit. Killing that process won't help — stop the container instead: docker ps → docker stop <container>.

PID 4 / System owns port 80 or 443 on Windows. That's http.sys. Don't kill it — find the reservation with netsh http show servicestate or stop the service using it (often IIS or the "World Wide Web Publishing Service").

Sister projects

  • eoldoctor — Git line-ending doctor
  • wholocks — find the process locking a file
  • longpath — fix Windows path-too-long trees
  • whichproxy — see whether a host uses your proxy
  • luanma — fix mojibake zip filenames

Development

git clone https://github.com/hc-ui/whoseport
cd whoseport
pip install -e ".[dev]"
pytest            # unit + real end-to-end tests (they bind sockets and kill child processes)
ruff check src tests

CI runs the suite on Linux, macOS and Windows across Python 3.9–3.14.

License

MIT