1import * as NetService from "@t3tools/shared/Net";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as CliError from "effect/cli/CliError";
6
7import { queryWindowsTcpListeners } from "../process/windowsTcpListeners.ts";
8import * as ProcessRunner from "../processRunner.ts";
9
10const KERNEL_RELEASE_PATH = "/proc/sys/kernel/osrelease";
11const WINDOWS_NETSTAT_PATH = String.raw`C:\Windows\System32\netstat.exe`;
12const FALLBACK_ATTEMPTS = 8;
13
14/**
15 * Picks the startup port when none was configured. Under WSL's default NAT networking, Windows
16 * reaches the server through localhost forwarding, which fails for a port a Windows process
17 * already listens on even though WSL can bind it.
18 */
19export const make = Effect.gen(function* () {
20 const net = yield* NetService.NetService;
21 const fs = yield* FileSystem.FileSystem;
22 const processRunner = yield* ProcessRunner.ProcessRunner;
23
24 // Services started by systemd lack WSL_DISTRO_NAME and WSL_INTEROP; the kernel release does not.
25 // Hosts without this file (macOS, Windows) are not WSL.
26 const isWsl = fs.readFileString(KERNEL_RELEASE_PATH).pipe(
27 Effect.map((release) => release.toLowerCase().includes("microsoft")),
28 Effect.orElseSucceed(() => false),
29 );
30
31 const readWindowsHostListeners = Effect.gen(function* () {
32 // The automount root is configurable, so `/mnt/c` is not guaranteed.
33 const netstat = yield* processRunner.run({
34 command: "wslpath",
35 args: ["-u", WINDOWS_NETSTAT_PATH],
36 timeout: Duration.seconds(5),
37 });
38 const netstatPath = netstat.stdout.trim();
39 if ((netstat.code !== null && netstat.code !== 0) || netstatPath.length === 0) {
40 return null;
41 }
42 const listeners = yield* queryWindowsTcpListeners(processRunner, netstatPath);
43 if (listeners === null) return null;
44 return new Set(listeners.map((listener) => listener.port));
45 }).pipe(Effect.orElseSucceed(() => null));
46
47 return Effect.fn("automaticPort.select")(function* (preferred: number) {
48 if (!(yield* isWsl)) return yield* net.findAvailablePort(preferred);
49 const windows = yield* readWindowsHostListeners;
50 if (windows === null) {
51 yield* Effect.logWarning(
52 "Could not list Windows TCP listeners from WSL; Windows may be unable to reach the selected port. Pass --port to choose one.",
53 );
54 return yield* net.findAvailablePort(preferred);
55 }
56 if (!windows.has(preferred) && (yield* net.isPortAvailableOnLoopback(preferred))) {
57 return preferred;
58 }
59 // Linux and Windows ephemeral ranges overlap, so fallback ports can collide too.
60 for (let attempt = 0; attempt < FALLBACK_ATTEMPTS; attempt += 1) {
61 const port = yield* net.reserveLoopbackPort();
62 if (!windows.has(port)) return port;
63 }
64 return yield* new CliError.UserError({
65 cause:
66 "Windows is already using every port T3 Code tried. Pass --port or set T3CODE_PORT to a port Windows is not using.",
67 });
68 });
69});