Skip to content
Closed
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
58 changes: 25 additions & 33 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,26 @@ import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";

const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
const MAX_TCP_PORT = 65_535;
const DESKTOP_BACKEND_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::"] as const;

const makeDesktopRunId = Crypto.Crypto.pipe(
Effect.flatMap((crypto) => crypto.randomUUIDv4),
Effect.map((value) => value.replaceAll("-", "").slice(0, 12)),
);

export class DesktopBackendPortUnavailableError extends Schema.TaggedErrorClass<DesktopBackendPortUnavailableError>()(
"DesktopBackendPortUnavailableError",
export class DesktopBackendPortInUseError extends Schema.TaggedErrorClass<DesktopBackendPortInUseError>()(
"DesktopBackendPortInUseError",
{
startPort: Schema.Int,
maxPort: Schema.Int,
port: Schema.Int,
hosts: Schema.Array(Schema.String),
},
) {
override get message(): string {
return `No desktop backend port is available on hosts ${this.hosts.join(", ")} between ${this.startPort} and ${this.maxPort}.`;
return [
`Desktop backend port ${this.port} is already in use on ${this.hosts.join(", ")}.`,
"T3 Code will not start another backend on a fallback port while using the same data directory.",
"Connect to the running T3 Code server, stop it cleanly, or use a different T3CODE_HOME.",
].join("\n");
}
}

Expand All @@ -66,40 +68,31 @@ const { logInfo: logBootstrapInfo, logWarning: logBootstrapWarning } =
const { logInfo: logStartupInfo, logError: logStartupError } =
DesktopObservability.makeComponentLogger("desktop-startup");

const resolveDesktopBackendPort = Effect.fn("resolveDesktopBackendPort")(function* (
export const resolveDesktopBackendPort = Effect.fn("resolveDesktopBackendPort")(function* (
configuredPort: Option.Option<number>,
) {
if (Option.isSome(configuredPort)) {
return {
port: configuredPort.value,
selectedByScan: false,
} as const;
}

const net = yield* NetService.NetService;
for (let port = DEFAULT_DESKTOP_BACKEND_PORT; port <= MAX_TCP_PORT; port += 1) {
let availableOnEveryHost = true;

for (const host of DESKTOP_BACKEND_PORT_PROBE_HOSTS) {
if (!(yield* net.canListenOnHost(port, host))) {
availableOnEveryHost = false;
break;
}
}

if (availableOnEveryHost) {
return {
port,
selectedByScan: true,
} as const;
const unavailableHosts: string[] = [];
for (const host of DESKTOP_BACKEND_PORT_PROBE_HOSTS) {
if (!(yield* net.canListenOnHost(DEFAULT_DESKTOP_BACKEND_PORT, host))) {
unavailableHosts.push(host);
}
}

return yield* new DesktopBackendPortUnavailableError({
startPort: DEFAULT_DESKTOP_BACKEND_PORT,
maxPort: MAX_TCP_PORT,
hosts: DESKTOP_BACKEND_PORT_PROBE_HOSTS,
});
if (unavailableHosts.length > 0) {
return yield* new DesktopBackendPortInUseError({
port: DEFAULT_DESKTOP_BACKEND_PORT,
hosts: unavailableHosts,
});
}
return {
port: DEFAULT_DESKTOP_BACKEND_PORT,
} as const;
});

const handleFatalStartupError = Effect.fn("desktop.startup.handleFatalStartupError")(function* (
Expand Down Expand Up @@ -157,12 +150,11 @@ const bootstrap = Effect.gen(function* () {
const backendPortSelection = yield* resolveDesktopBackendPort(environment.configuredBackendPort);
const backendPort = backendPortSelection.port;
yield* logBootstrapInfo(
backendPortSelection.selectedByScan
? "selected backend port via sequential scan"
: "using configured backend port",
Option.isSome(environment.configuredBackendPort)
? "using configured backend port"
: "using default backend port",
{
port: backendPort,
...(backendPortSelection.selectedByScan ? { startPort: DEFAULT_DESKTOP_BACKEND_PORT } : {}),
},
);

Expand Down
74 changes: 64 additions & 10 deletions apps/desktop/src/app/DesktopAppErrors.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,81 @@
import * as NetService from "@t3tools/shared/Net";
import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";

import {
DesktopBackendPortUnavailableError,
DesktopBackendPortInUseError,
DesktopDevelopmentBackendPortRequiredError,
resolveDesktopBackendPort,
} from "./DesktopApp.ts";

const netLayer = (canListenOnHost: (port: number, host: string) => boolean) =>
Layer.succeed(NetService.NetService, {
canListenOnHost: (port, host) => Effect.succeed(canListenOnHost(port, host)),
isPortAvailableOnLoopback: () => Effect.die("unexpected isPortAvailableOnLoopback"),
hasListenerOnHost: () => Effect.die("unexpected hasListenerOnHost"),
reserveLoopbackPort: () => Effect.die("unexpected reserveLoopbackPort"),
findAvailablePort: () => Effect.die("unexpected findAvailablePort"),
} satisfies NetService.NetService["Service"]);

describe("DesktopApp errors", () => {
it("preserves unavailable backend port context", () => {
const error = new DesktopBackendPortUnavailableError({
startPort: 3_773,
maxPort: 65_535,
hosts: ["127.0.0.1", "0.0.0.0", "::"],
it("preserves occupied default-port context", () => {
const error = new DesktopBackendPortInUseError({
port: 3_773,
hosts: ["127.0.0.1"],
});

assert.equal(error.startPort, 3_773);
assert.equal(error.maxPort, 65_535);
assert.deepEqual(error.hosts, ["127.0.0.1", "0.0.0.0", "::"]);
assert.equal(error.port, 3_773);
assert.deepEqual(error.hosts, ["127.0.0.1"]);
assert.equal(
error.message,
"No desktop backend port is available on hosts 127.0.0.1, 0.0.0.0, :: between 3773 and 65535.",
[
"Desktop backend port 3773 is already in use on 127.0.0.1.",
"T3 Code will not start another backend on a fallback port while using the same data directory.",
"Connect to the running T3 Code server, stop it cleanly, or use a different T3CODE_HOME.",
].join("\n"),
);
});

it.effect("uses the default port when every bind host is available", () =>
Effect.gen(function* () {
const selection = yield* resolveDesktopBackendPort(Option.none());
assert.deepEqual(selection, { port: 3_773 });
}).pipe(Effect.provide(netLayer(() => true))),
);

it.effect("refuses instead of scanning to another same-home port", () => {
const probes: Array<{ readonly port: number; readonly host: string }> = [];
return Effect.gen(function* () {
const failure = yield* resolveDesktopBackendPort(Option.none()).pipe(Effect.flip);
assert.strictEqual(failure._tag, "DesktopBackendPortInUseError");
if (failure._tag === "DesktopBackendPortInUseError") {
assert.strictEqual(failure.port, 3_773);
assert.deepEqual(failure.hosts, ["127.0.0.1"]);
}
assert.deepEqual(probes, [
{ port: 3_773, host: "127.0.0.1" },
{ port: 3_773, host: "0.0.0.0" },
{ port: 3_773, host: "::" },
]);
}).pipe(
Effect.provide(
netLayer((port, host) => {
probes.push({ port, host });
return host !== "127.0.0.1";
}),
),
);
});

it.effect("preserves an explicitly configured backend port", () =>
Effect.gen(function* () {
const selection = yield* resolveDesktopBackendPort(Option.some(9_999));
assert.deepEqual(selection, { port: 9_999 });
}).pipe(Effect.provide(netLayer(() => false))),
);

it("reports the required development port", () => {
const error = new DesktopDevelopmentBackendPortRequiredError();

Expand Down
Loading