1import * as NodeNet from "node:net";
2
3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { assert, describe, it } from "@effect/vitest";
5import * as HostProcess from "@t3tools/shared/HostProcess";
6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";
8import * as Stream from "effect/Stream";
9import { ChildProcess, ChildProcessSpawner } from "effect/process";
10
11// On macOS, Node can bind 127.0.0.1:<port> while another process (such as the
12// desktop app with network access on) listens on *:<port>. Linux refuses that
13// bind unless both sockets set SO_REUSEPORT, so on Linux the preload adds it to
14// the picker's probe and the holder sets it too. macOS needs neither.
15const MACOS_BIND_PRELOAD = `const net = require("node:net");
16const listen = net.Server.prototype.listen;
17net.Server.prototype.listen = function (port, host, callback) {
18 if (typeof port !== "number" || typeof host !== "string") return listen.apply(this, arguments);
19 return listen.call(this, { port, host, reusePort: true }, callback);
20};
21`;
22
23describe("__ssh-helper pick-port", () => {
24 const hostPlatform = HostProcess.Platform.defaultValue();
25 const linuxHost = hostPlatform === "linux";
26 const windowsHost = hostPlatform === "win32";
27
28 const holdWildcardPort = Effect.acquireRelease(
29 Effect.callback<NodeNet.Server>((resume) => {
30 const server = NodeNet.createServer((socket) => socket.destroy());
31 server.once("error", (error) => resume(Effect.die(error)));
32 server.listen({ port: 0, host: "0.0.0.0", reusePort: linuxHost }, () =>
33 resume(Effect.succeed(server)),
34 );
35 }),
36 (server) =>
37 Effect.callback<void>((resume) => {
38 server.close(() => resume(Effect.void));
39 }),
40 );
41
42 it.effect.skipIf(windowsHost)(
43 "skips a port another server answers on loopback",
44 () =>
45 Effect.gen(function* () {
46 const fs = yield* FileSystem.FileSystem;
47 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
48 const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pick-port-" });
49 yield* fs.writeFileString(`${root}/preload.cjs`, MACOS_BIND_PRELOAD);
50 const holder = yield* holdWildcardPort;
51 const heldPort = (holder.address() as NodeNet.AddressInfo).port;
52
53 const child = yield* spawner.spawn(
54 ChildProcess.make(process.execPath, [
55 ...(linuxHost ? ["--require", `${root}/preload.cjs`] : []),
56 "--experimental-strip-types",
57 new URL("../bin.ts", import.meta.url).pathname,
58 "__ssh-helper",
59 "pick-port",
60 "",
61 String(heldPort),
62 "20",
63 ]),
64 );
65 const [stdout, exitCode] = yield* Effect.all(
66 [
67 child.stdout.pipe(
68 Stream.decodeText(),
69 Stream.runFold(
70 () => "",
71 (acc, chunk) => acc + chunk,
72 ),
73 ),
74 child.exitCode.pipe(Effect.map(Number)),
75 ],
76 { concurrency: "unbounded" },
77 );
78
79 assert.equal(exitCode, 0);
80 const picked = Number(stdout);
81 assert.isAbove(picked, heldPort);
82 assert.isBelow(picked, heldPort + 20);
83 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
84 30_000,
85 );
86});