335 lines · 13.6 KB · TypeScript
RawDownload
import * as Cause from "effect/Cause";import * as Effect from "effect/Effect";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";import * as ElectronProtocol from "../electron/ElectronProtocol.ts";import * as ElectronSafeStorage from "../electron/ElectronSafeStorage.ts";import { installDesktopIpcHandlers } from "../ipc/DesktopIpcHandlers.ts";import * as DesktopAppActivation from "./DesktopAppActivation.ts";import * as DesktopAppIdentity from "./DesktopAppIdentity.ts";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 DesktopEnvironment from "./DesktopEnvironment.ts";import * as DesktopLifecycle from "./DesktopLifecycle.ts";import * as DesktopLinuxUrlHandler from "./DesktopLinuxUrlHandler.ts";import * as DesktopObservability from "./DesktopObservability.ts";import * as DesktopPreReadyPlatform from "./DesktopPreReadyPlatform.ts";import * as DesktopShutdown from "./DesktopShutdown.ts";import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";import * as DesktopShellEnvironment from "../shell/DesktopShellEnvironment.ts";import * as DesktopState from "./DesktopState.ts";import * as DesktopRemoteUpdates from "../updates/DesktopRemoteUpdates.ts";import * as DesktopUpdates from "../updates/DesktopUpdates.ts";import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.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.TaggedError<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.TaggedError<DesktopDevelopmentBackendPortRequiredError>()(  "DesktopDevelopmentBackendPortRequiredError",  {},) {  override get message(): string {    return "T3CODE_PORT is required in desktop development.";  }} const { logInfo: logBootstrapInfo, logWarning: logBootstrapWarning } =  DesktopObservability.makeComponentLogger("desktop-bootstrap"); 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,): Effect.fn.Return<  void,  never,  | DesktopShutdown.DesktopShutdown  | DesktopState.DesktopState  | ElectronApp.ElectronApp  | ElectronDialog.ElectronDialog> {  const shutdown = yield* DesktopShutdown.DesktopShutdown;  const state = yield* DesktopState.DesktopState;  const electronApp = yield* ElectronApp.ElectronApp;  const electronDialog = yield* ElectronDialog.ElectronDialog;  const message = error instanceof Error ? error.message : String(error);  const detail =    error instanceof Error && typeof error.stack === "string" ? `\n${error.stack}` : "";  yield* logStartupError("fatal startup error", {    stage,    message,    ...(detail.length > 0 ? { detail } : {}),  });  const wasQuitting = yield* Ref.getAndSet(state.quitting, true);  if (!wasQuitting) {    yield* electronDialog.showErrorBox(      "T3 Code failed to start",      `Stage: ${stage}\n${message}${detail}`,    );  }  yield* shutdown.request;  yield* electronApp.quit;}); const fatalStartupCause = <E>(stage: string, cause: Cause.Cause<E>) =>  handleFatalStartupError(stage, Cause.pretty(cause)).pipe(Effect.andThen(Effect.failCause(cause))); const bootstrap = Effect.gen(function* () {  const pool = yield* DesktopBackendPool.DesktopBackendPool;  const primaryBackend = yield* pool.primary;  const state = yield* DesktopState.DesktopState;  const environment = yield* DesktopEnvironment.DesktopEnvironment;  const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;  const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;  const wslBackend = yield* DesktopWslBackend.DesktopWslBackend;  const desktopWindow = yield* DesktopWindow.DesktopWindow;  const snapShot = yield* DesktopSnapShot.DesktopSnapShot;  const appActivation = yield* DesktopAppActivation.DesktopAppActivation;  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;  if (settings.serverExposureMode !== environment.defaultDesktopSettings.serverExposureMode) {    yield* logBootstrapInfo("bootstrap restoring persisted server exposure mode", {      mode: settings.serverExposureMode,    });  }  const serverExposureState = yield* serverExposure.configureFromSettings({ port: backendPort });  const backendConfig = yield* serverExposure.backendConfig;  const electronProtocol = yield* ElectronProtocol.ElectronProtocol;  const rendererTarget = environment.isDevelopment    ? Option.getOrThrow(environment.devServerUrl)    : backendConfig.httpBaseUrl;  yield* electronProtocol.registerDesktopProtocol({    scheme: ElectronProtocol.getDesktopScheme(environment.isDevelopment),    targetOrigin: rendererTarget,    backendOrigin: backendConfig.httpBaseUrl,    clerkFrontendApiHostname: DesktopClerk.desktopClerkFrontendApiHostname,  });  yield* logBootstrapInfo("bootstrap resolved backend endpoint", {    baseUrl: backendConfig.httpBaseUrl.href,  });  if (serverExposureState.endpointUrl) {    yield* logBootstrapInfo("bootstrap enabled network access", {      endpointUrl: serverExposureState.endpointUrl,    });  } else if (    settings.serverExposureMode === "network-accessible" &&    serverExposureState.mode === "local-only"  ) {    yield* logBootstrapWarning(      "bootstrap fell back to local-only because no advertised network host was available",    );  }  yield* snapShot.initialize;   yield* installDesktopIpcHandlers();  yield* logBootstrapInfo("bootstrap ipc handlers registered");   if (!(yield* Ref.get(state.quitting))) {    // In wsl-only mode the renderer is served by the WSL backend, which can be    // slow to cold-boot — show a "Connecting to WSL" splash immediately so the    // app feels responsive instead of presenting no window until WSL is ready.    // (Dual mode opens fast off the Windows primary, so no splash there.)    if (settings.wslOnly === true && settings.wslBackendEnabled === true) {      yield* desktopWindow.showConnectingSplash;    }    yield* primaryBackend.start;    yield* logBootstrapInfo("bootstrap backend start requested");    yield* appActivation.start.pipe(      Effect.tap(() => logBootstrapInfo("desktop app control socket ready")),      Effect.catch((error) => logStartupError("desktop app control socket unavailable", { error })),    );    // Bring up the WSL backend if the user previously enabled it. The    // primary is already starting; reconcile fires off the WSL register    // in parallel rather than blocking primary readiness on a possibly    // slow first wsl.exe spawn.    yield* Effect.forkScoped(wslBackend.reconcile);  }}).pipe(Effect.withSpan("desktop.bootstrap")); const startup = Effect.gen(function* () {  const appIdentity = yield* DesktopAppIdentity.DesktopAppIdentity;  const applicationMenu = yield* DesktopApplicationMenu.DesktopApplicationMenu;  const electronApp = yield* ElectronApp.ElectronApp;  const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;  const linuxUrlHandler = yield* DesktopLinuxUrlHandler.DesktopLinuxUrlHandler;  const clerk = yield* DesktopClerk.DesktopClerk;  const shellEnvironment = yield* DesktopShellEnvironment.DesktopShellEnvironment;  const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;  const preReadyElectronOptions = yield* DesktopPreReadyPlatform.DesktopPreReadyElectronOptions;  const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage;  const updates = yield* DesktopUpdates.DesktopUpdates;  const environment = yield* DesktopEnvironment.DesktopEnvironment;   yield* shellEnvironment.installIntoProcess;  const hasCommandLinePasswordStore =    preReadyElectronOptions.linuxPasswordStoreCommandLine !== null;  const linuxElectronOptions =    environment.platform === "linux" && !hasCommandLinePasswordStore      ? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess()      : preReadyElectronOptions.linux;  if (linuxElectronOptions !== null && !hasCommandLinePasswordStore) {    if (      linuxElectronOptions.passwordStore !== null ||      preReadyElectronOptions.linux?.passwordStore !== null    ) {      yield* electronApp.removeCommandLineSwitch("password-store");    }    if (linuxElectronOptions.passwordStore !== null) {      yield* electronApp.appendCommandLineSwitch(        "password-store",        linuxElectronOptions.passwordStore,      );    }  }  const userDataPath = yield* appIdentity.resolveUserDataPath;  yield* electronApp.setPath("userData", userDataPath);  yield* logStartupInfo("runtime logging configured", { logDir: environment.logDir });  yield* desktopSettings.load;   if (linuxElectronOptions !== null) {    yield* logStartupInfo("linux password store configured", {      passwordStore: hasCommandLinePasswordStore        ? "command-line"        : (linuxElectronOptions.passwordStore ?? "electron-default"),      xdgCurrentDesktop: process.env.XDG_CURRENT_DESKTOP ?? null,      xdgSessionDesktop: process.env.XDG_SESSION_DESKTOP ?? null,    });  }   yield* appIdentity.configure;  yield* lifecycle.register;  yield* clerk.configure;   yield* electronApp.whenReady.pipe(    Effect.withSpan("desktop.electron.whenReady"),    Effect.catchCause((cause) => fatalStartupCause("whenReady", cause)),  );  yield* logStartupInfo("app ready");  if (environment.platform === "linux") {    const selectedBackend = yield* safeStorage.selectedStorageBackend;    yield* logStartupInfo("safe storage ready", {      backend: Option.getOrElse(selectedBackend, () => "unknown"),    });  }  yield* appIdentity.configure;  yield* applicationMenu.configure;  yield* updates.configure;  yield* DesktopRemoteUpdates.listen;  yield* linuxUrlHandler.register;  yield* bootstrap.pipe(Effect.catchCause((cause) => fatalStartupCause("bootstrap", cause)));}).pipe(Effect.withSpan("desktop.startup")); const scopedProgram = Effect.scoped(  Effect.gen(function* () {    const runId = yield* makeDesktopRunId;    yield* Effect.annotateLogsScoped({ scope: "desktop", runId });    yield* Effect.annotateCurrentSpan({ scope: "desktop", runId });     const shutdown = yield* DesktopShutdown.DesktopShutdown;     yield* Effect.addFinalizer(() =>      Effect.gen(function* () {        const pool = yield* DesktopBackendPool.DesktopBackendPool;        // Stop every backend in the pool, not just the primary. The        // electronApp.quit() path can race ahead of the layer-scope        // cascade, so leaving the WSL instance for its parent scope        // finalizer means it gets hard-killed by the OS instead of        // receiving SIGTERM + grace. Stops run concurrently.        const instances = yield* pool.list;        yield* Effect.forEach(instances, (instance) => instance.stop(), {          concurrency: "unbounded",        });      }).pipe(Effect.ensuring(shutdown.markComplete)),    );     yield* startup;    yield* shutdown.awaitRequest;  }),); export const program = scopedProgram.pipe(Effect.withSpan("desktop.app"));