Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 20 additions & 23 deletions lib/common/services/net-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,35 +48,32 @@ export class Net implements INet {
}

public async isPortAvailable(port: number): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
let isResolved = false;
const server = net.createServer();
// A listener on one address does not stop a bind on another (e.g. the
// wildcard next to the loopback), so each of them has to be free.
for (const host of ["127.0.0.1", "::1", "0.0.0.0", "::"]) {
if (!(await this.canListen(port, host))) {
return false;
}
}

server.on("error", (err: Error) => {
if (!isResolved) {
isResolved = true;
resolve(false);
}
});
return true;
}

server.once("close", () => {
if (!isResolved) {
// "close" will be emitted right after "error"
isResolved = true;
resolve(true);
}
});
private canListen(port: number, host: string): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const server = net.createServer();

server.on("listening", (err: Error) => {
if (err && !isResolved) {
isResolved = true;
resolve(true);
}
server.once("error", (err: NodeJS.ErrnoException) => {
// The host has no such address (e.g. IPv6 is disabled), so nothing
// can be listening on it either.
resolve(err.code === "EADDRNOTAVAIL" || err.code === "EAFNOSUPPORT");
});

server.close();
server.once("listening", () => {
server.close(() => resolve(true));
});

server.listen(port, "localhost");
server.listen(port, host);
});
}

Expand Down
38 changes: 38 additions & 0 deletions lib/common/test/unit-tests/services/net-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Net } from "../../../services/net-service";
import { assert } from "chai";
import * as net from "net";
import { Yok } from "../../../yok";
import { ErrorsStub, CommonLoggerStub } from "../stubs";
import { EOL } from "os";
Expand All @@ -25,6 +26,43 @@ describe("net", () => {
return testInjector;
};

describe("isPortAvailable", () => {
const hosts = [undefined, "127.0.0.1", "::1", "0.0.0.0", "::"];

for (const host of hosts) {
it(`returns false for a port taken on ${host || "the default address"}`, async (context: any) => {
const service = createTestInjector("darwin").resolve<INet>(Net);
const server = net.createServer();
const listening = await new Promise<boolean>((resolve) => {
server.once("error", () => resolve(false));
const onListening = () => resolve(true);
host ? server.listen(0, host, onListening) : server.listen(0, onListening);
});
if (!listening) {
context.skip();
return;
}
const { port } = <net.AddressInfo>server.address();

try {
assert.isFalse(await service.isPortAvailable(port));
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

it("returns true for a free port", async () => {
const service = createTestInjector("darwin").resolve<INet>(Net);
const server = net.createServer();
await new Promise<void>((resolve) => server.listen(0, () => resolve()));
const { port } = <net.AddressInfo>server.address();
await new Promise((resolve) => server.close(resolve));

assert.isTrue(await service.isPortAvailable(port));
});
});

describe("waitForPortToListen", () => {
let execCalledCount = 0;
beforeEach(() => {
Expand Down
Loading
Loading