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
120 changes: 51 additions & 69 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import * as Schema from "effect/Schema";

import * as NetService from "@t3tools/shared/Net";
import * as Crypto from "effect/Crypto";
import * as ElectronApp from "../electron/ElectronApp.ts";
import * as ElectronDialog from "../electron/ElectronDialog.ts";
Expand All @@ -16,6 +15,8 @@ import * as DesktopClerk from "./DesktopClerk.ts";
import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";
import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
import * as DesktopBackendPort from "../backend/DesktopBackendPort.ts";
import * as DesktopExistingLocalBackendStartup from "../backend/DesktopExistingLocalBackendStartup.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopLifecycle from "./DesktopLifecycle.ts";
import * as DesktopLinuxUrlHandler from "./DesktopLinuxUrlHandler.ts";
Expand All @@ -29,28 +30,11 @@ import * as DesktopState from "./DesktopState.ts";
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",
{
startPort: Schema.Int,
maxPort: 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}.`;
}
}

export class DesktopDevelopmentBackendPortRequiredError extends Schema.TaggedErrorClass<DesktopDevelopmentBackendPortRequiredError>()(
"DesktopDevelopmentBackendPortRequiredError",
{},
Expand All @@ -66,42 +50,6 @@ const { logInfo: logBootstrapInfo, logWarning: logBootstrapWarning } =
const { logInfo: logStartupInfo, logError: logStartupError } =
DesktopObservability.makeComponentLogger("desktop-startup");

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;
}
}

return yield* new DesktopBackendPortUnavailableError({
startPort: DEFAULT_DESKTOP_BACKEND_PORT,
maxPort: MAX_TCP_PORT,
hosts: DESKTOP_BACKEND_PORT_PROBE_HOSTS,
});
});

const handleFatalStartupError = Effect.fn("desktop.startup.handleFatalStartupError")(function* (
stage: string,
error: unknown,
Expand Down Expand Up @@ -148,25 +96,47 @@ const bootstrap = Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const wslBackend = yield* DesktopWslBackend.DesktopWslBackend;
const desktopWindow = yield* DesktopWindow.DesktopWindow;
const shutdown = yield* DesktopShutdown.DesktopShutdown;
const electronApp = yield* ElectronApp.ElectronApp;
yield* logBootstrapInfo("bootstrap start");

if (environment.isDevelopment && Option.isNone(environment.configuredBackendPort)) {
return yield* new DesktopDevelopmentBackendPortRequiredError();
}

const backendPortSelection = yield* resolveDesktopBackendPort(environment.configuredBackendPort);
const backendPort = backendPortSelection.port;
yield* logBootstrapInfo(
backendPortSelection.selectedByScan
? "selected backend port via sequential scan"
: "using configured backend port",
{
port: backendPort,
...(backendPortSelection.selectedByScan ? { startPort: DEFAULT_DESKTOP_BACKEND_PORT } : {}),
},
);

const settings = yield* desktopSettings.get;
const existingLocalBackendSelection =
yield* DesktopExistingLocalBackendStartup.resolveExistingLocalBackendForStartup;
if (existingLocalBackendSelection._tag === "Quit") {
yield* Ref.set(state.quitting, true);
yield* shutdown.request;
yield* electronApp.quit;
return;
}
const existingLocalBackend = existingLocalBackendSelection.attachment;
const backendPortSelection = Option.isSome(existingLocalBackend)
? ({ port: existingLocalBackend.value.backend.port, selectedByScan: false } as const)
: yield* DesktopBackendPort.resolveDesktopBackendPort(environment.configuredBackendPort);
const backendPort = backendPortSelection.port;
if (Option.isSome(existingLocalBackend)) {
yield* logBootstrapInfo("attaching to existing local backend", {
origin: existingLocalBackend.value.backend.origin,
port: existingLocalBackend.value.backend.port,
baseDir: existingLocalBackend.value.backend.baseDir,
});
} else {
yield* logBootstrapInfo(
backendPortSelection.selectedByScan
? "selected backend port via sequential scan"
: "using configured backend port",
{
port: backendPort,
...(backendPortSelection.selectedByScan
? { startPort: DesktopBackendPort.DEFAULT_DESKTOP_BACKEND_PORT }
: {}),
},
);
}
if (settings.serverExposureMode !== environment.defaultDesktopSettings.serverExposureMode) {
yield* logBootstrapInfo("bootstrap restoring persisted server exposure mode", {
mode: settings.serverExposureMode,
Expand All @@ -175,17 +145,29 @@ const bootstrap = Effect.gen(function* () {
const serverExposureState = yield* serverExposure.configureFromSettings({ port: backendPort });
const backendConfig = yield* serverExposure.backendConfig;
const electronProtocol = yield* ElectronProtocol.ElectronProtocol;
const backendOrigin = Option.match(existingLocalBackend, {
onNone: () => backendConfig.httpBaseUrl,
onSome: ({ backend }) => new URL(backend.origin),
});
const rendererTarget = environment.isDevelopment
? Option.getOrThrow(environment.devServerUrl)
: backendConfig.httpBaseUrl;
: Option.match(existingLocalBackend, {
onNone: () => backendConfig.httpBaseUrl,
onSome: ({ backend }) => new URL(backend.origin),
});
yield* electronProtocol.registerDesktopProtocol({
scheme: ElectronProtocol.getDesktopScheme(environment.isDevelopment),
targetOrigin: rendererTarget,
backendOrigin: backendConfig.httpBaseUrl,
backendOrigin,
...(environment.isDevelopment || Option.isNone(existingLocalBackend)
? {}
: {
rendererRoot: environment.path.join(environment.serverRoot, "apps/server/dist/client"),
}),
clerkFrontendApiHostname: DesktopClerk.desktopClerkFrontendApiHostname,
});
yield* logBootstrapInfo("bootstrap resolved backend endpoint", {
baseUrl: backendConfig.httpBaseUrl.href,
baseUrl: backendOrigin.href,
});
if (serverExposureState.endpointUrl) {
yield* logBootstrapInfo("bootstrap enabled network access", {
Expand Down
6 changes: 2 additions & 4 deletions apps/desktop/src/app/DesktopAppErrors.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { assert, describe, it } from "@effect/vitest";

import {
DesktopBackendPortUnavailableError,
DesktopDevelopmentBackendPortRequiredError,
} from "./DesktopApp.ts";
import { DesktopBackendPortUnavailableError } from "../backend/DesktopBackendPort.ts";
import { DesktopDevelopmentBackendPortRequiredError } from "./DesktopApp.ts";

describe("DesktopApp errors", () => {
it("preserves unavailable backend port context", () => {
Expand Down
Loading
Loading