1// @effect-diagnostics nodeBuiltinImport:off
2// @effect-diagnostics globalTimers:off
3// @effect-diagnostics globalDateInEffect:off
4// The helpers mirror the inline Node snippets the SSH launch script used to
5// run, byte for byte in behaviour, so they stay on plain Node APIs.
6import * as NodeFS from "node:fs";
7import * as NodeHttp from "node:http";
8import * as NodeNet from "node:net";
9
10import * as Effect from "effect/Effect";
11import { Argument, Command } from "effect/cli";
12
13/**
14 * Small helpers the SSH launch script needs on the remote host. The script
15 * used to run these as inline `node -` snippets; archive-distributed runtimes
16 * have no Node on the remote, so the executable provides them instead. Output
17 * and exit codes match the snippets exactly because the shell script parses
18 * them.
19 */
20
21const canBind = (port: number) =>
22 new Promise<boolean>((resolve) => {
23 const server = NodeNet.createServer();
24 server.unref();
25 server.once("error", () => resolve(false));
26 server.listen(port, "127.0.0.1", () => {
27 server.close((error) => resolve(!error));
28 });
29 });
30
31// macOS lets the bind above succeed while another process listens on
32// *:<port>, so also skip any port that already answers on loopback.
33const answersOnLoopback = (port: number) =>
34 new Promise<boolean>((resolve) => {
35 const socket = NodeNet.connect({ port, host: "127.0.0.1" });
36 socket.setTimeout(1000);
37 socket.once("connect", () => {
38 socket.destroy();
39 resolve(true);
40 });
41 socket.once("timeout", () => {
42 socket.destroy();
43 resolve(true);
44 });
45 socket.once("error", () => resolve(false));
46 });
47
48/** Prints the first free loopback port from the preferred one, scanning `window` ports. */
49const pickPort = Command.make("pick-port", {
50 portFile: Argument.String("port-file"),
51 defaultPort: Argument.Int("default-port"),
52 scanWindow: Argument.Int("scan-window"),
53}).pipe(
54 Command.withHandler(({ portFile, defaultPort, scanWindow }) =>
55 Effect.promise(async () => {
56 const raw = NodeFS.existsSync(portFile) ? NodeFS.readFileSync(portFile, "utf8").trim() : "";
57 const preferred = Number.parseInt(raw, 10);
58 const start = Number.isInteger(preferred) ? preferred : defaultPort;
59 for (let port = start; port < start + scanWindow; port += 1) {
60 if ((await canBind(port)) && !(await answersOnLoopback(port))) {
61 process.stdout.write(String(port));
62 return;
63 }
64 }
65 process.exitCode = 1;
66 }),
67 ),
68);
69
70const probe = (port: number, probeTimeoutMs: number) =>
71 new Promise<boolean>((resolve) => {
72 const request = NodeHttp.get(
73 { hostname: "127.0.0.1", port, path: "/", timeout: probeTimeoutMs },
74 (response) => {
75 response.resume();
76 response.once("end", () => {
77 const status = response.statusCode ?? 0;
78 resolve(status >= 200 && status < 300);
79 });
80 },
81 );
82 request.once("timeout", () => {
83 request.destroy();
84 resolve(false);
85 });
86 request.once("error", () => resolve(false));
87 });
88
89/** Exits 0 once the loopback server answers, 1 when the deadline passes first. */
90const waitReady = Command.make("wait-ready", {
91 port: Argument.Int("port"),
92 timeoutMs: Argument.Int("timeout-ms"),
93 probeTimeoutMs: Argument.Int("probe-timeout-ms"),
94}).pipe(
95 Command.withHandler(({ port, timeoutMs, probeTimeoutMs }) =>
96 Effect.promise(async () => {
97 const deadline = Date.now() + timeoutMs;
98 while (Date.now() < deadline) {
99 if (await probe(port, probeTimeoutMs)) return;
100 await new Promise((resolve) => setTimeout(resolve, 100));
101 }
102 process.exitCode = 1;
103 }),
104 ),
105);
106
107/** Prints `<pid> <port>` for a live default-home server, or exits 1. */
108const runtimePort = Command.make("runtime-port", {
109 runtimeFile: Argument.String("runtime-file"),
110}).pipe(
111 Command.withHandler(({ runtimeFile }) =>
112 Effect.sync(() => {
113 try {
114 // @effect-diagnostics-next-line preferSchemaOverJson:off - mirrors the shell snippet's loose parse.
115 const runtime = JSON.parse(NodeFS.readFileSync(runtimeFile, "utf8")) as {
116 pid?: unknown;
117 port?: unknown;
118 origin?: unknown;
119 };
120 const pid = Number(runtime.pid);
121 const port = Number(runtime.port);
122 if (!Number.isInteger(pid) || pid <= 0 || !Number.isInteger(port)) {
123 process.exitCode = 1;
124 return;
125 }
126 const origin = new URL(String(runtime.origin ?? ""));
127 if (origin.protocol !== "http:" || !["127.0.0.1", "localhost"].includes(origin.hostname)) {
128 process.exitCode = 1;
129 return;
130 }
131 process.kill(pid, 0);
132 process.stdout.write(`${pid} ${port}`);
133 } catch {
134 process.exitCode = 1;
135 }
136 }),
137 ),
138);
139
140export const sshHelperCommand = Command.make("__ssh-helper").pipe(
141 Command.unlisted,
142 Command.withSubcommands([pickPort, waitReady, runtimePort]),
143);