arelos 0.2.0 → 0.2.1
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.
- package/dist/ports.js +81 -13
- package/package.json +1 -1
package/dist/ports.js
CHANGED
|
@@ -1,24 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zero-dependency free-port detection via node:net (spec §1 Step 5).
|
|
3
3
|
*
|
|
4
|
-
* A port must be probed on
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* A port must be probed on every listener shape a process can occupy it
|
|
5
|
+
* with, not just the specific loopback addresses:
|
|
6
|
+
*
|
|
7
|
+
* - A server listening only on [::1] (IPv6) leaves 127.0.0.1 bindable, so an
|
|
8
|
+
* IPv4-only probe reports the port "free" while browsers (which resolve
|
|
9
|
+
* localhost to ::1 first) hit the other process. Field-tested: a Node dev
|
|
10
|
+
* server on [::1]:1347 made the installer pre-fill an occupied port.
|
|
11
|
+
* - A server listening on the WILDCARD address (e.g. `Bun.serve()` with no
|
|
12
|
+
* `hostname`, or `net.createServer().listen(port)` with no host) binds
|
|
13
|
+
* every local address in that family, including both loopback addresses,
|
|
14
|
+
* yet a bind-probe of 127.0.0.1/::1 alone can still report "free" on
|
|
15
|
+
* macOS: binding a specific address with SO_REUSEADDR (Node's default) can
|
|
16
|
+
* succeed even though a wildcard listener already holds the port. Real
|
|
17
|
+
* incident: `isPortFree(5274)` reported free while a Bun server held
|
|
18
|
+
* `*:5274`, and the installer wrote that occupied port into config.
|
|
19
|
+
*
|
|
20
|
+
* The fix probes both shapes with two independent techniques:
|
|
21
|
+
* 1. Bind-probes on the specific loopback addresses AND the wildcard
|
|
22
|
+
* addresses for each family. A wildcard bind-probe fails with
|
|
23
|
+
* EADDRINUSE whenever *any* listener (wildcard or specific) holds the
|
|
24
|
+
* port in that family — it is the strongest bind test available.
|
|
25
|
+
* 2. A short-timeout TCP connect-probe to 127.0.0.1 and ::1. A successful
|
|
26
|
+
* connection proves the port is occupied even in edge cases the bind
|
|
27
|
+
* probes can't reach; ECONNREFUSED proves free for that family; a
|
|
28
|
+
* timeout or other inconclusive result is never treated as occupied on
|
|
29
|
+
* its own.
|
|
30
|
+
*
|
|
31
|
+
* A port counts as free only if every applicable bind probe succeeds AND no
|
|
32
|
+
* connect probe manages to connect.
|
|
10
33
|
*/
|
|
11
|
-
import { createServer } from "node:net";
|
|
34
|
+
import { createServer, connect as netConnect } from "node:net";
|
|
12
35
|
/** Error codes meaning "this address family isn't available on this machine". */
|
|
13
36
|
const FAMILY_UNAVAILABLE = new Set(["EADDRNOTAVAIL", "EAFNOSUPPORT", "EINVAL"]);
|
|
14
|
-
/** Try to bind `port` on one
|
|
37
|
+
/** Try to bind `port` on one host (specific address or wildcard) and report what happened. */
|
|
15
38
|
function probeBind(port, host) {
|
|
16
39
|
return new Promise((resolve) => {
|
|
17
40
|
const server = createServer();
|
|
18
41
|
server.once("error", (err) => {
|
|
19
42
|
server.close();
|
|
20
43
|
if (err.code && FAMILY_UNAVAILABLE.has(err.code)) {
|
|
21
|
-
// No IPv6 (or IPv4)
|
|
44
|
+
// No IPv6 (or IPv4) support on this machine — nothing to conflict with.
|
|
22
45
|
resolve("family-unavailable");
|
|
23
46
|
}
|
|
24
47
|
else {
|
|
@@ -33,13 +56,58 @@ function probeBind(port, host) {
|
|
|
33
56
|
server.listen(port, host);
|
|
34
57
|
});
|
|
35
58
|
}
|
|
59
|
+
const CONNECT_PROBE_TIMEOUT_MS = 250;
|
|
60
|
+
/**
|
|
61
|
+
* Try to TCP-connect to `host:port` with a short timeout. A successful
|
|
62
|
+
* connection means something is definitely listening there. ECONNREFUSED
|
|
63
|
+
* means nothing is listening (free, for that family). Anything else
|
|
64
|
+
* (timeout, unreachable, family unavailable) is inconclusive and must not be
|
|
65
|
+
* treated as proof of occupation by itself.
|
|
66
|
+
*/
|
|
67
|
+
function probeConnect(port, host) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
let settled = false;
|
|
70
|
+
let socket;
|
|
71
|
+
const finish = (result) => {
|
|
72
|
+
if (settled)
|
|
73
|
+
return;
|
|
74
|
+
settled = true;
|
|
75
|
+
socket.destroy();
|
|
76
|
+
resolve(result);
|
|
77
|
+
};
|
|
78
|
+
socket = netConnect({ port, host });
|
|
79
|
+
socket.setTimeout(CONNECT_PROBE_TIMEOUT_MS);
|
|
80
|
+
socket.once("connect", () => finish("occupied"));
|
|
81
|
+
socket.once("timeout", () => finish("inconclusive"));
|
|
82
|
+
socket.once("error", (err) => {
|
|
83
|
+
if (err.code === "ECONNREFUSED") {
|
|
84
|
+
finish("free");
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// EHOSTUNREACH, ENETUNREACH, family unavailable, etc. — inconclusive.
|
|
88
|
+
finish("inconclusive");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
36
93
|
/**
|
|
37
|
-
* Resolve true if `port` is free
|
|
38
|
-
*
|
|
94
|
+
* Resolve true if `port` is free on every probe: bind-probes on the specific
|
|
95
|
+
* loopback addresses and the wildcard addresses for both families, plus
|
|
96
|
+
* connect-probes to the loopback addresses. False if any bind probe reports
|
|
97
|
+
* "taken" or any connect probe successfully connects.
|
|
39
98
|
*/
|
|
40
99
|
export async function isPortFree(port) {
|
|
41
|
-
const [
|
|
42
|
-
|
|
100
|
+
const [v4Loopback, v6Loopback, v4Wildcard, v6Wildcard, v4Connect, v6Connect] = await Promise.all([
|
|
101
|
+
probeBind(port, "127.0.0.1"),
|
|
102
|
+
probeBind(port, "::1"),
|
|
103
|
+
probeBind(port, "0.0.0.0"),
|
|
104
|
+
probeBind(port, "::"),
|
|
105
|
+
probeConnect(port, "127.0.0.1"),
|
|
106
|
+
probeConnect(port, "::1"),
|
|
107
|
+
]);
|
|
108
|
+
const bindsOk = [v4Loopback, v6Loopback, v4Wildcard, v6Wildcard].every((result) => result !== "taken");
|
|
109
|
+
const noConnectSucceeded = v4Connect !== "occupied" && v6Connect !== "occupied";
|
|
110
|
+
return bindsOk && noConnectSucceeded;
|
|
43
111
|
}
|
|
44
112
|
/**
|
|
45
113
|
* Find the first free port at or above `start`, scanning upward. Stops at
|