Skip to content

Commit d6c36f9

Browse files
committed
Check all interfaces
Inspired by the implementation in https://github.com/sindresorhus/get-port/blob/0760c987c17581395d4e30432881dcb0ca6ca94a/index.js, make sure the port is available on all interfaces.
1 parent 9e1d84d commit d6c36f9

File tree

2 files changed

+124
-5
lines changed

2 files changed

+124
-5
lines changed

source/index.ts

+56-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import crypto from 'node:crypto';
22
import net from 'node:net';
3+
import os from 'node:os';
34
import {SharedContext} from '@ava/cooperate';
45

56
const context = new SharedContext(import.meta.url);
67

8+
const localHosts = new Set([
9+
undefined, // Default interfaces,
10+
'0.0.0.0', // Ensure we check IPv4,
11+
...Object.values(os.networkInterfaces()).flatMap(interfaces => interfaces?.map(info => info.address)),
12+
]);
13+
714
// Reserve a range of 16 addresses at a random offset.
815
const reserveRange = async (): Promise<number[]> => {
916
let from: number;
@@ -15,24 +22,69 @@ const reserveRange = async (): Promise<number[]> => {
1522
return context.reserve(...range);
1623
};
1724

25+
const enum Availability {
26+
AVAILABLE,
27+
UNAVAILABLE,
28+
UNKNOWN,
29+
}
30+
1831
// Listen on the port to make sure it's available.
19-
const confirmAvailable = async (port: number, options?: net.ListenOptions): Promise<boolean> => new Promise((resolve, reject) => {
32+
const confirmAvailableForHost = async ({
33+
host,
34+
listenOptions,
35+
port,
36+
unknowable,
37+
}: {
38+
host: string | undefined;
39+
listenOptions?: net.ListenOptions;
40+
port: number;
41+
unknowable: boolean;
42+
}): Promise<Availability> => new Promise((resolve, reject) => {
2043
const server = net.createServer();
2144
server.unref();
2245
server.on('error', (error: Error & {code: string}) => {
2346
if (error.code === 'EADDRINUSE' || error.code === 'EACCESS') {
24-
resolve(false);
47+
resolve(Availability.UNAVAILABLE);
48+
} else if (unknowable && (error.code === 'EADDRNOTAVAIL' || error.code === 'EINVAL')) { // https://github.com/sindresorhus/get-port/blob/0760c987c17581395d4e30432881dcb0ca6ca94a/index.js#L65
49+
resolve(Availability.UNKNOWN); // The address itself is not available, so we can't check.
2550
} else {
2651
reject(error);
2752
}
2853
});
29-
server.listen({...options, port}, () => {
54+
server.listen({...listenOptions, host, port}, () => {
3055
server.close(() => {
31-
resolve(true);
56+
resolve(Availability.AVAILABLE);
3257
});
3358
});
3459
});
3560

61+
const confirmAvailable = async (port: number, listenOptions?: net.ListenOptions): Promise<boolean> => {
62+
if (listenOptions?.host !== undefined) {
63+
const available = await confirmAvailableForHost({
64+
host: listenOptions.host,
65+
listenOptions,
66+
port,
67+
unknowable: false,
68+
});
69+
return available === Availability.AVAILABLE;
70+
}
71+
72+
for await (const host of localHosts) {
73+
const available = await confirmAvailableForHost({
74+
host,
75+
listenOptions,
76+
port,
77+
unknowable: true,
78+
});
79+
80+
if (available === Availability.UNAVAILABLE) {
81+
return false;
82+
}
83+
}
84+
85+
return true;
86+
};
87+
3688
let available: Promise<number[]> = reserveRange();
3789
export default async function getPort(options?: Omit<net.ListenOptions, 'port'>): Promise<number> {
3890
const promise = available;

test/test.ts

+68-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import net from 'node:net';
2+
import os from 'node:os';
23
import {promisify} from 'node:util';
34
import test from 'ava';
45
import getPort from '../source/index.js';
56

6-
test('gets up to 16 ports in a block', async t => {
7+
test.serial('gets up to 16 ports in a block', async t => {
78
const first = await getPort();
89
let count = 1;
910

@@ -23,6 +24,72 @@ test('gets up to 16 ports in a block', async t => {
2324
t.log({count, first, newBlock});
2425
});
2526

27+
function * range(from: number, to: number) {
28+
for (let i = from; i < to; i++) {
29+
yield i;
30+
}
31+
}
32+
33+
test.serial('skips used ports', async t => {
34+
const first = await getPort();
35+
t.log({first});
36+
37+
let attempt;
38+
for await (const i of range(1, 16)) {
39+
attempt?.discard();
40+
attempt = await t.try(async tt => {
41+
const reserved = first + i;
42+
tt.log({reserved});
43+
44+
const server = net.createServer();
45+
tt.teardown(() => server.close());
46+
47+
const listen: (port: number) => Promise<void> = promisify(server.listen.bind(server));
48+
await listen(reserved);
49+
50+
const port = await getPort();
51+
tt.log({port});
52+
tt.true(port > reserved);
53+
});
54+
55+
if (attempt.passed) {
56+
break;
57+
}
58+
}
59+
60+
attempt?.commit();
61+
});
62+
63+
test.serial('fails on invalid hosts', async t => {
64+
let code;
65+
let host;
66+
for await (const info of Object.values(os.networkInterfaces()).flatMap(interfaces => interfaces ?? [])) {
67+
const server = net.createServer();
68+
t.teardown(() => server.close());
69+
70+
const unavailable = await new Promise<{code: 'EADDRNOTAVAIL' | 'EINVAL'; host: string} | undefined>((resolve, reject) => {
71+
server.on('error', (error: Error & {code: string}) => {
72+
if (error.code === 'EADDRNOTAVAIL' || error.code === 'EINVAL') {
73+
resolve({code: error.code, host: info.address});
74+
} else {
75+
reject(error);
76+
}
77+
});
78+
server.listen({host: info.address, port: 0}, () => {
79+
resolve(undefined);
80+
});
81+
});
82+
83+
if (unavailable !== undefined) {
84+
({code, host} = unavailable);
85+
break;
86+
}
87+
}
88+
89+
t.not(host, undefined);
90+
await t.throwsAsync(getPort({host}), {code});
91+
});
92+
2693
test('port can be bound', async t => {
2794
const server = net.createServer();
2895
t.teardown(() => server.close());

0 commit comments

Comments
 (0)