whoisport 0.5.2 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +132 -63
  2. package/bin/whoisport.js +24 -12
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -2,10 +2,25 @@
2
2
 
3
3
  [![crates.io](https://img.shields.io/crates/v/whoisport.svg)](https://crates.io/crates/whoisport)
4
4
  [![CI](https://github.com/dpologdvinity/whoisport/actions/workflows/ci.yml/badge.svg)](https://github.com/dpologdvinity/whoisport/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
6
 
6
- Find out what is listening on a TCP or UDP port, and optionally kill it.
7
+ **Find out what is listening on a port, who started it, and stop it safely.**
8
+ Works on Linux, macOS and Windows. One small binary, no runtime dependencies.
7
9
 
8
- ![whoisport demo](demo.gif)
10
+ ![whoisport demo](https://raw.githubusercontent.com/dpologdvinity/whoisport/master/demo.gif)
11
+
12
+ `lsof -i :3000` gives you a PID and a wall of columns. `whoisport` gives you the command
13
+ line, the directory it was started from, who owns it and how long it has been running,
14
+ which is usually what you need to decide whether to kill it. And when you do kill it,
15
+ whoisport makes sure it only stops the process you were looking at.
16
+
17
+ ## Quickstart
18
+
19
+ ```sh
20
+ whoisport # what is listening on this machine?
21
+ whoisport 3000 # what is on port 3000, and who started it?
22
+ whoisport 3000 --kill # ask it to exit (add --force if it refuses)
23
+ ```
9
24
 
10
25
  ```console
11
26
  $ whoisport 3000
@@ -29,88 +44,142 @@ PORT PID USER CMD
29
44
  ports marked ? are owned by another user; run with sudo to see it
30
45
  ```
31
46
 
32
- `lsof -i :3000` gives you a PID and a wall of columns. `whoisport` gives you the
33
- command line, the directory it was started from, who owns it and how long it has
34
- been running, which is usually what you need to decide whether to kill it.
35
-
36
47
  ## Install
37
48
 
38
- ```sh
39
- cargo binstall whoisport # prebuilt binary, no compiling
40
- cargo install whoisport # build from source
41
- pipx install whoisport # or: uvx whoisport, pip install whoisport
42
- ```
49
+ | Platform | Command |
50
+ |----------|---------|
51
+ | Any, with Rust | `cargo binstall whoisport` (prebuilt) or `cargo install whoisport` (builds from source) |
52
+ | Any, with Python | `pipx install whoisport` or `uvx whoisport` |
53
+ | macOS, Linux | `brew install dpologdvinity/tap/whoisport` |
54
+ | Windows | `scoop bucket add whoisport https://github.com/dpologdvinity/scoop-whoisport` then `scoop install whoisport` |
55
+ | Debian, Ubuntu | download the `.deb` from [Releases](https://github.com/dpologdvinity/whoisport/releases), then `sudo apt install ./whoisport_*.deb` |
56
+ | Fedora, RHEL | download the `.rpm` from [Releases](https://github.com/dpologdvinity/whoisport/releases), then `sudo dnf install ./whoisport-*.rpm` |
43
57
 
44
- macOS or Linux, with [Homebrew](https://brew.sh):
58
+ Or download a binary from the [Releases page](https://github.com/dpologdvinity/whoisport/releases):
59
+ Linux (static, x86_64 and aarch64), macOS (Apple silicon and Intel) and Windows (x86_64).
45
60
 
46
- ```sh
47
- brew install dpologdvinity/tap/whoisport
48
- ```
61
+ **Supported systems:** Linux (reads `/proc`, no other tools needed), macOS (uses the
62
+ built-in `lsof`), Windows 10 and 11.
49
63
 
50
- Debian/Ubuntu or Fedora/RHEL: download the `.deb` or `.rpm` for your architecture
51
- from the [Releases page](https://github.com/dpologdvinity/whoisport/releases), then
64
+ ## Usage
52
65
 
53
- ```sh
54
- sudo apt install ./whoisport_*.deb # Debian, Ubuntu
55
- sudo dnf install ./whoisport-*.rpm # Fedora, RHEL
66
+ ```
67
+ whoisport [PORTS]... [--kill [--force]] [--json] [--udp]
56
68
  ```
57
69
 
58
- Windows, with [Scoop](https://scoop.sh):
59
-
60
- ```powershell
61
- scoop bucket add whoisport https://github.com/dpologdvinity/whoisport
62
- scoop install whoisport
70
+ | You type | You get |
71
+ |----------|---------|
72
+ | `whoisport` | A table of every listening port |
73
+ | `whoisport 3000 8080` | Details of what is on each port (repeated ports are shown once) |
74
+ | `whoisport 3000 --kill` | The details, then the process is asked to exit and given 3 seconds |
75
+ | `whoisport 3000 --kill --force` | Same, but forcibly ended if it does not exit |
76
+ | `whoisport --json` | The same data as a JSON array, for scripts |
77
+ | `whoisport --udp` | UDP sockets instead of TCP |
78
+
79
+ `--kill` always needs explicit ports; it never means "kill everything". `--json` cannot
80
+ be combined with `--kill`.
81
+
82
+ ### JSON output
83
+
84
+ `--json` prints an array with one object per listener. Every object has the same keys;
85
+ what is unknown (for example the owner of a process you may not inspect) is `null`:
86
+
87
+ ```json
88
+ [
89
+ {
90
+ "port": 3000, "proto": "tcp", "pid": 373232, "ppid": 373230, "user": "kaitlyn",
91
+ "cmd": "node node_modules/.bin/vite --port 3000", "cwd": "/home/kaitlyn/site",
92
+ "uptime_secs": 252, "container": null, "image": null
93
+ }
94
+ ]
63
95
  ```
64
96
 
65
- Or download a binary from the
66
- [Releases page](https://github.com/dpologdvinity/whoisport/releases): Linux (static,
67
- x86_64 and aarch64), macOS (Apple Silicon and Intel) and Windows (x86_64).
97
+ ### Exit codes
68
98
 
69
- Works on **Linux** (reads `/proc` directly, no runtime dependencies), **macOS**
70
- (uses the built-in `lsof`) and **Windows 10/11**.
99
+ | Code | Meaning |
100
+ |------|---------|
101
+ | `0` | Success: everything asked for was found (and killed, if asked) |
102
+ | `1` | A requested port has nothing listening, or nothing is listening at all |
103
+ | `2` | Invalid arguments |
104
+ | `3` | A kill failed, or a process holding the port was deliberately not killed (see below), so the port may still be in use |
71
105
 
72
- ## Usage
106
+ ### Reading `?`
73
107
 
74
- ```
75
- whoisport # list every listening port
76
- whoisport <PORT>... # details for one or more ports
77
- whoisport <PORT>... --kill [--force]
78
- whoisport [<PORT>...] --json # machine-readable output
79
- whoisport [<PORT>...] --udp # UDP instead of TCP
80
- ```
108
+ On Linux, a process owned by another user cannot be inspected without `sudo`, so its row
109
+ shows `?`. On macOS `lsof` only lists your own processes without `sudo`, so other
110
+ users' listeners are missing. On Windows, details of system services need an elevated
111
+ terminal.
112
+
113
+ ## Stopping processes safely
114
+
115
+ `--kill` is built so that it cannot take down more than you meant to:
116
+
117
+ - **The process you saw is the process that is signalled.** Each target is identified by
118
+ its pid *and* its start time. If it exits and the system hands its pid to something
119
+ else, that something else is left alone.
120
+ - **Service managers and parents are never signalled.** When several processes share a
121
+ socket (a supervisor and its workers, or `systemd` socket activation), stopping the
122
+ supervisor takes down far more than one port. `systemd`, `launchd` and `svchost.exe`,
123
+ and any process that is a parent of another holder, are skipped. whoisport then says
124
+ which one still holds the port and exits `3`.
125
+ - **It asks first.** Each target is asked to exit; it is forced only with `--force`.
126
+
127
+ | Platform | `--kill` asks by | `--force` |
128
+ |----------|------------------|-----------|
129
+ | Linux, macOS | `SIGTERM` | `SIGKILL` |
130
+ | Windows | Ctrl+C in the process's console, and `WM_CLOSE` to its windows | `TerminateProcess` |
131
+
132
+ On Windows, Ctrl+C reaches every program attached to a console, so whoisport only sends
133
+ it when the target's console holds nothing but the target, its child processes and a
134
+ shell. Otherwise it says so and leaves the choice of `--force` to you.
81
135
 
82
- | Flag | Effect |
83
- |-----------|---------------------------------------------------------------------|
84
- | `--kill` | Ask the process to exit and wait up to 3 seconds. |
85
- | `--force` | With `--kill`: forcibly terminate it if asking did not work. |
86
- | `--json` | Print a JSON array with `port`, `proto`, `pid`, `ppid`, `user`, `cmd`, `cwd`, `uptime_secs`, `container`, `image`. |
87
- | `--udp` | Look at UDP sockets instead of TCP. UDP has no listening state, so any bound socket without a remote peer counts. Windows doesn't report UDP peers, so there every bound UDP socket is listed, including short-lived client ones. |
136
+ Text from other processes (command lines, paths) is printed with control characters,
137
+ bidirectional-text overrides and zero-width characters escaped, so a hostile process
138
+ cannot rewrite your terminal or disguise what it is.
88
139
 
89
- Docker: when a port is published by a container, whoisport shows the container
90
- name and image (`docker web (nginx:alpine)`), even when the `docker-proxy`
91
- process itself is owned by root. It asks `docker ps` only in that case.
140
+ ## Docker
92
141
 
93
- `--kill` always needs explicit ports; it never kills everything.
142
+ When a port is published by a container, whoisport shows the container name and image
143
+ (`docker web (nginx:alpine)`), even though the `docker-proxy` process behind it is owned
144
+ by root. Docker is consulted only when a listener looks like Docker's forwarder or its
145
+ owner cannot be read, only for a local daemon (not an `ssh://` or remote `DOCKER_HOST`),
146
+ and each Docker call is cut off after 2 seconds, so a stuck daemon never hangs
147
+ whoisport: the names are simply left out. `--kill` on such a port prints the matching
148
+ `docker stop <name>` command, because killing the forwarder only breaks the port mapping.
94
149
 
95
- How `--kill` asks, per platform:
150
+ ## UDP
96
151
 
97
- | Platform | `--kill` | `--force` |
98
- |----------|-----------------------------------------------------------------|--------------------|
99
- | Linux, macOS | `SIGTERM` | `SIGKILL` |
100
- | Windows | Ctrl+C in the process's console, and `WM_CLOSE` to its windows | `TerminateProcess` |
152
+ UDP has no listening state, so `--udp` lists bound sockets that have no remote peer.
153
+ Windows does not report UDP peers, so there every bound UDP socket is listed, including
154
+ short-lived client ones.
101
155
 
102
- On Windows, Ctrl+C reaches every program attached to a console, so whoisport only
103
- sends it when the target's console holds nothing but the target, its child
104
- processes and a shell. Otherwise it says so and leaves the choice of `--force` to you.
156
+ ## How it works
105
157
 
106
- Exit codes: `0` success, `1` a requested port had nothing listening (or nothing
107
- is listening at all), `2` invalid arguments, `3` a kill failed.
158
+ A short version (the long one is in [ARCHITECTURE.md](ARCHITECTURE.md)): whoisport asks
159
+ the operating system which sockets are listening and which process owns each one, looks
160
+ up details of those processes, and prints them. Each platform has its own small backend
161
+ for that (`/proc` on Linux, `lsof` on macOS, the IP Helper API on Windows); everything
162
+ else is shared.
163
+
164
+ ```
165
+ cli ─► scan ─► output (list, details, JSON)
166
+ │
167
+ └───► kill (identity, safety rules, ask-then-force)
168
+ ▲
169
+ proc backend per OS · docker names
170
+ ```
171
+
172
+ ## Development
173
+
174
+ ```sh
175
+ cargo test # unit and end-to-end tests
176
+ cargo clippy --all-targets -- -D warnings
177
+ ```
108
178
 
109
- Checks IPv4 and IPv6 TCP sockets in the LISTEN state. On Linux, processes owned
110
- by other users can't be inspected without `sudo`; they show up as `?`. On
111
- Windows, details of system services need an elevated terminal. On macOS,
112
- other users' listeners only appear when run with `sudo`.
179
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow (formatting, coverage,
180
+ checking the other platforms, commit style) and [ARCHITECTURE.md](ARCHITECTURE.md) for
181
+ how the code is organised.
113
182
 
114
183
  ## License
115
184
 
116
- MIT
185
+ [MIT](LICENSE)
package/bin/whoisport.js CHANGED
@@ -1,33 +1,45 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- // Runs the prebuilt whoisport binary from the platform package that npm (or pnpm,
4
- // yarn, bun) installed as an optional dependency.
3
+ /**
4
+ * whoisport launcher.
5
+ *
6
+ * The `whoisport` npm package contains no binary of its own. Each platform's binary lives
7
+ * in its own package (`whoisport-linux-x64`, `whoisport-windows-x64`, ...), listed as an
8
+ * optional dependency with `os`/`cpu` set, so npm, pnpm, yarn and bun install only the one
9
+ * that matches this machine. This script finds that package, runs its binary with the same
10
+ * arguments, and passes the exit code (or terminating signal) straight through.
11
+ */
5
12
  const { spawnSync } = require("node:child_process");
6
13
 
14
+ /** Platform + architecture, as `process.platform process.arch`, to npm package name. */
7
15
  const packages = {
8
16
  "linux x64": "whoisport-linux-x64",
9
17
  "linux arm64": "whoisport-linux-arm64",
10
18
  "darwin x64": "whoisport-darwin-x64",
11
19
  "darwin arm64": "whoisport-darwin-arm64",
12
- "win32 x64": "whoisport-win32-x64",
20
+ "win32 x64": "whoisport-windows-x64", // Node calls Windows "win32"; the package is named "windows"
13
21
  };
14
- const name = packages[`${process.platform} ${process.arch}`];
15
- if (!name) {
16
- console.error(`whoisport: no prebuilt binary for ${process.platform}-${process.arch}`);
22
+
23
+ /** Print `message` to stderr and exit with failure. */
24
+ function fail(message) {
25
+ console.error(`whoisport: ${message}`);
17
26
  process.exit(1);
18
27
  }
28
+
29
+ const name = packages[`${process.platform} ${process.arch}`];
30
+ if (!name) fail(`no prebuilt binary for ${process.platform}-${process.arch}`);
31
+
19
32
  const exe = process.platform === "win32" ? "whoisport.exe" : "whoisport";
20
33
  let bin;
21
34
  try {
35
+ // `require.resolve` finds the binary inside the platform package's install directory.
22
36
  bin = require.resolve(`${name}/bin/${exe}`);
23
37
  } catch {
24
- console.error(`whoisport: ${name} is not installed (optional dependencies skipped?)`);
25
- process.exit(1);
38
+ fail(`${name} is not installed (were optional dependencies skipped?)`);
26
39
  }
40
+
27
41
  const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
28
- if (result.error) {
29
- console.error(`whoisport: ${result.error.message}`);
30
- process.exit(1);
31
- }
42
+ if (result.error) fail(result.error.message);
43
+ // Re-raise the child's signal so the shell sees the same cause of death.
32
44
  if (result.signal) process.kill(process.pid, result.signal);
33
45
  process.exit(result.status ?? 1);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "whoisport",
3
3
  "description": "Show what is listening on a TCP or UDP port, and optionally kill it",
4
- "version": "0.5.2",
4
+ "version": "0.5.6",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -28,10 +28,10 @@
28
28
  "node": ">=16"
29
29
  },
30
30
  "optionalDependencies": {
31
- "whoisport-linux-x64": "0.5.2",
32
- "whoisport-linux-arm64": "0.5.2",
33
- "whoisport-darwin-x64": "0.5.2",
34
- "whoisport-darwin-arm64": "0.5.2",
35
- "whoisport-win32-x64": "0.5.2"
31
+ "whoisport-linux-x64": "0.5.6",
32
+ "whoisport-linux-arm64": "0.5.6",
33
+ "whoisport-darwin-x64": "0.5.6",
34
+ "whoisport-darwin-arm64": "0.5.6",
35
+ "whoisport-windows-x64": "0.5.6"
36
36
  }
37
37
  }