Part VI · Client architectures: shared semantics, platform edgesElectron desktop
Chapter 32source checked

Electron desktop: one renderer, explicit native authority

The desktop reuses the web renderer but not browser authority: Electron main assembles effects, supervises local backend children, exposes a narrow preload bridge, and separately brokers WSL, SSH, previews, menus, updates, telemetry, and shutdown.

What this chapter resolves
  • Locate authority across Electron main, preload, renderer, local backend children, WSL, and SSH.
  • Trace desktop boot through endpoint selection, protocol and IPC setup, backend readiness, and window creation.
  • Distinguish the host-local primary and Windows-only WSL pool instances from an SSH remote-environment gateway.
  • Read fd3, fd4, and fd5 as separate bootstrap, telemetry, and diagnostics-demand channels.

Electron desktop is not “the web app in a window.” The renderer shares much of the web client, but its extra powers remain on the other side of two boundaries: Electron main owns native APIs and child processes; preload exposes a curated set of IPC calls; renderer renders product state and asks through that bridge. The environment server still owns the workbench and orchestration services described in earlier chapters.

1. Authority is layered before it is interactive

The main entry builds an Electron service layer, desktop foundation services, server-exposure and preview services, window services, the backend pool, the WSL orchestrator, local-environment authentication, then application services including lifecycle, menu, shell, SSH, and updates. Strict pre-ready configuration wraps the runtime so Electron options that must precede readiness are acquired before ordinary startup work.

At startup, main installs the shell environment, resolves early Linux password-store options, selects Electron user data, loads settings, configures app identity and lifecycle/Clerk hooks, then awaits Electron readiness. Only after that does it configure identity again, application menu, updates, Linux URL handling, and the desktop bootstrap. This is a dependency order, not an instruction for a renderer to call native modules.

The preload uses contextBridge.exposeInMainWorld to publish desktopBridge with specific operations: local bootstrap credentials, connection catalog, SSH discovery and connection, server exposure and WSL settings, file/dialog helpers, menu/window events, update actions/state, and preview actions. It does not publish raw ipcRenderer, Node process spawning, or Electron BrowserWindow control to page code. Main registers the IPC handlers before the backend is started.

apps/desktop/src/preload.ts:30–103 ↗verbatim · typescript · f1c239b6
contextBridge.exposeInMainWorld("desktopBridge", {
  getAppBranding: () => {
    const result = ipcRenderer.sendSync(IpcChannels.GET_APP_BRANDING_CHANNEL);
    if (typeof result !== "object" || result === null) {
      return null;
    }
    return result as ReturnType<DesktopBridge["getAppBranding"]>;
  },
  getSystemLocale: () => {
    const result = ipcRenderer.sendSync(IpcChannels.GET_SYSTEM_LOCALE_CHANNEL);
    return typeof result === "string" ? result : null;
  },
  getLocalEnvironmentBootstraps: () => {
    const result = ipcRenderer.sendSync(IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL);
    if (!Array.isArray(result)) {
      return [];
    }
    return result as ReturnType<DesktopBridge["getLocalEnvironmentBootstraps"]>;
  },
  getLocalEnvironmentBearerToken: () =>
    ipcRenderer.invoke(IpcChannels.GET_LOCAL_ENVIRONMENT_BEARER_TOKEN_CHANNEL),
  getClientSettings: () => ipcRenderer.invoke(IpcChannels.GET_CLIENT_SETTINGS_CHANNEL),
  setClientSettings: (settings) =>
    ipcRenderer.invoke(IpcChannels.SET_CLIENT_SETTINGS_CHANNEL, settings),
  getConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.GET_CONNECTION_CATALOG_CHANNEL),
  setConnectionCatalog: (catalog) =>
    ipcRenderer.invoke(IpcChannels.SET_CONNECTION_CATALOG_CHANNEL, catalog),
  clearConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.CLEAR_CONNECTION_CATALOG_CHANNEL),
  discoverSshHosts: () => ipcRenderer.invoke(IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL),
  ensureSshEnvironment: async (target, options) =>
    unwrapEnsureSshEnvironmentResult(
      await ipcRenderer.invoke(IpcChannels.ENSURE_SSH_ENVIRONMENT_CHANNEL, {
        target,
        ...(options === undefined ? {} : { options }),
      }),
    ),
  disconnectSshEnvironment: (target) =>
    ipcRenderer.invoke(IpcChannels.DISCONNECT_SSH_ENVIRONMENT_CHANNEL, target),
  fetchSshEnvironmentDescriptor: (httpBaseUrl) =>
    ipcRenderer.invoke(IpcChannels.FETCH_SSH_ENVIRONMENT_DESCRIPTOR_CHANNEL, { httpBaseUrl }),
  bootstrapSshBearerSession: (httpBaseUrl, credential) =>
    ipcRenderer.invoke(IpcChannels.BOOTSTRAP_SSH_BEARER_SESSION_CHANNEL, {
      httpBaseUrl,
      credential,
    }),
  fetchSshSessionState: (httpBaseUrl, bearerToken) =>
    ipcRenderer.invoke(IpcChannels.FETCH_SSH_SESSION_STATE_CHANNEL, { httpBaseUrl, bearerToken }),
  issueSshWebSocketTicket: (httpBaseUrl, bearerToken) =>
    ipcRenderer.invoke(IpcChannels.ISSUE_SSH_WEBSOCKET_TOKEN_CHANNEL, { httpBaseUrl, bearerToken }),
  onSshPasswordPrompt: (listener) => {
    const wrappedListener = (_event: Electron.IpcRendererEvent, request: unknown) => {
      if (typeof request !== "object" || request === null) return;
      listener(request as Parameters<typeof listener>[0]);
    };
 
    ipcRenderer.on(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, wrappedListener);
    return () => {
      ipcRenderer.removeListener(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, wrappedListener);
    };
  },
  resolveSshPasswordPrompt: (requestId, password) =>
    ipcRenderer.invoke(IpcChannels.RESOLVE_SSH_PASSWORD_PROMPT_CHANNEL, { requestId, password }),
  getServerExposureState: () => ipcRenderer.invoke(IpcChannels.GET_SERVER_EXPOSURE_STATE_CHANNEL),
  setServerExposureMode: (mode) =>
    ipcRenderer.invoke(IpcChannels.SET_SERVER_EXPOSURE_MODE_CHANNEL, mode),
  setTailscaleServeEnabled: (input) =>
    ipcRenderer.invoke(IpcChannels.SET_TAILSCALE_SERVE_ENABLED_CHANNEL, input),
  getAdvertisedEndpoints: () => ipcRenderer.invoke(IpcChannels.GET_ADVERTISED_ENDPOINTS_CHANNEL),
  getWslState: () => ipcRenderer.invoke(IpcChannels.GET_WSL_STATE_CHANNEL),
  setWslBackendEnabled: (enabled) =>
    ipcRenderer.invoke(IpcChannels.SET_WSL_BACKEND_ENABLED_CHANNEL, enabled),
  setWslDistro: (distro) => ipcRenderer.invoke(IpcChannels.SET_WSL_DISTRO_CHANNEL, distro),
  setWslOnly: (enabled) => ipcRenderer.invoke(IpcChannels.SET_WSL_ONLY_CHANNEL, enabled),
  pickFolder: (options) => ipcRenderer.invoke(IpcChannels.PICK_FOLDER_CHANNEL, options),
Read this as: Preload publishes named operations rather than raw Electron or Node authority. Local bootstrap, SSH, exposure, WSL, and file selection all cross explicit IPC methods.

2. Bootstrap chooses an endpoint before it opens a window

Desktop bootstrap obtains the pool’s primary instance, selects a configured port or scans upward from the desktop default across 127.0.0.1, 0.0.0.0, and ::, then configures server exposure from persisted settings. Local-only binds loopback; network-accessible mode may bind broadly and advertises a usable LAN endpoint. If no LAN address is available, it falls back to local-only rather than pretending the network endpoint exists.

That resolved backend URL is also an input to desktop protocol registration: the renderer target is the development server in development and the backend HTTP origin in production, while the protocol knows the backend origin for its own routing rules. IPC handlers are installed next. In WSL-only mode the window service can show a connecting splash; otherwise the primary starts, and WSL reconciliation is forked so a slow wsl.exe cold start does not block the primary readiness path.

apps/desktop/src/app/DesktopApp.ts:142–218 ↗verbatim · typescript · 3382a448
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;
  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") {
    yield* logBootstrapWarning(
      "bootstrap fell back to local-only because no advertised network host was available",
    );
  }
 
  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");
    // 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);
  }
Read this as: Desktop resolves exposure and protocol origins, registers IPC, starts the selected primary pool instance, then reconciles an optional WSL secondary without blocking primary readiness.
Figure 32.1 · Desktop boot, steady state, and shutdown topologysolid arrows are process or transport paths; primary branches are mutually exclusive
Electron desktop process topology and lifecycleDiagram loading

Zoom with the controls, +/, or Ctrl/ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.

Electron desktop process topology and lifecycle
Text equivalent

Electron main owns the backend pool, window, menu, update, preview, telemetry, server-exposure, and SSH services. Preload is the renderer-to-main bridge. Primary configuration chooses either a host-local child on macOS, Linux, or Windows, or a WSL primary in Windows WSL-only mode. A host-local child receives bootstrap on fd3, host telemetry on fd4, and returns diagnostic-demand control on fd5. A WSL primary or optional dual-mode WSL secondary receives bootstrap on stdin because wsl.exe drops extra descriptors. Each local child has an HTTP readiness path. SSH flows from renderer through preload and Electron main to a remote T3 environment and is never a local pool child. Graceful shutdown stops every pool instance; SSH disconnection is separate.

Figure 32.1. Electron main configures exposure and the desktop protocol, registers IPC, and starts one primary pool instance. The ordinary primary is host-local; Windows WSL-only mode instead resolves the primary as WSL, while dual mode may register a separate WSL secondary. Host-local delivery uses fd3/fd4/fd5; WSL delivery uses stdin because wsl.exe drops extra descriptors. HTTP readiness gates each child. SSH remains a separately owned remote gateway.

3. The pool supervises local instances, not all environments

DesktopBackendPool makes one primary instance for its lifetime. It can register additional instances with a child scope; unregistering a non-primary closes that scope and invokes the instance’s stop finalizer. The pool refuses to unregister the primary. Each instance has independent configuration resolution, state, mutex, restart loop, readiness flag, and active child process. A ready primary calls the window service with its HTTP base URL; a ready WSL secondary does not replace that window callback.

The WSL orchestrator reconciles persisted enablement/distro settings after primary start and after its IPC settings changes. In dual mode, it allocates a distinct port, registers wsl:default or wsl:<distro>, and starts that child. It deliberately does nothing parallel in WSL-only mode because the primary itself is then resolved as WSL. The Linux child omits the Windows T3 home so it does not share the primary’s database, and the renderer discovers it through the desktop-local bootstrap/saved- environment path.

apps/desktop/src/backend/DesktopBackendConfiguration.ts:689–728 ↗verbatim · typescript · 58261db6
  // Single source of truth for what the primary actually runs as. Both
  // the start-config dispatch and the renderer-facing label derive from
  // this, so they can't disagree — e.g. the label reading "WSL" while the
  // config silently fell back to Windows because WSL is unavailable.
  // Dispatch happens at resolve time so toggling wsl-only between restarts
  // is picked up on the next start cycle (the pool's primary instance is
  // created once at layer init, but configResolve fires on each restart).
  const describePrimary = Effect.gen(function* () {
    const persistedSettings = yield* settings.get;
    const wslRequested = persistedSettings.wslOnly && persistedSettings.wslBackendEnabled;
    // Only honor wsl-only when WSL is actually usable. If the user
    // persisted wsl-only but WSL has since become unavailable (wsl.exe
    // removed, no distro), fall back to the Windows primary instead of
    // looping forever on preflight failures: the Connections backend
    // control is hidden while WSL is unavailable, so a stuck WSL primary
    // would otherwise leave no in-app way back to Windows.
    const useWsl = wslRequested && (yield* wslEnvironment.isAvailable);
    return { useWsl, wslRequested, distro: persistedSettings.wslDistro };
  });
 
  return DesktopBackendConfiguration.of({
    resolvePrimary: Effect.gen(function* () {
      const { useWsl, wslRequested } = yield* describePrimary;
      if (useWsl) {
        return yield* buildWslPrimaryConfig;
      }
      if (wslRequested) {
        yield* Effect.logWarning(
          "WSL-only backend requested but WSL is unavailable; starting the Windows primary instead.",
        );
      }
      return yield* buildWindowsPrimaryConfig;
    }).pipe(Effect.withSpan("desktop.backendConfiguration.resolvePrimary")),
    resolvePrimaryLabel: Effect.gen(function* () {
      const { useWsl, distro } = yield* describePrimary;
      if (!useWsl) {
        return environment.platform === "win32" ? "Windows" : "Local environment";
      }
      return distro ? `WSL (${distro})` : "WSL";
    }).pipe(Effect.withSpan("desktop.backendConfiguration.resolvePrimaryLabel")),
Read this as: The primary is a long-lived pool identity whose configuration is resolved again on restart. Non-Windows hosts label it Local environment; Windows can select WSL-only when available or fall back to Windows.
apps/desktop/src/backend/DesktopBackendPool.ts:327–419 ↗verbatim · typescript · 6cc62e74
              return Effect.fail(
                new DesktopBackendPoolInstanceAlreadyRegisteredError({ id: spec.id }),
              );
            }
            if (existing?._tag === "Closing") {
              return Effect.succeed([
                { _tag: "Wait", done: existing.done } as const,
                current,
              ] as const);
            }
            return Effect.gen(function* () {
              // Provide the captured factory services first, then the child scope
              // last so instance finalizers are owned by the unregisterable scope.
              const instanceScope = yield* Scope.fork(layerScope, "sequential");
              const instance = yield* DesktopBackendManager.makeBackendInstance(spec).pipe(
                Effect.provide(factoryContext),
                Scope.provide(instanceScope),
              );
              const next = new Map(current);
              next.set(spec.id, {
                _tag: "Active",
                instance,
                scope: Option.some(instanceScope),
              });
              return [
                { _tag: "Registered", instance } as const,
                next as ReadonlyMap<BackendInstanceId, RegisteredInstance>,
              ] as const;
            });
          },
        ).pipe(
          Effect.flatMap((result) =>
            result._tag === "Registered"
              ? Effect.succeed(result.instance)
              : Deferred.await(result.done).pipe(Effect.andThen(register(spec))),
          ),
        ),
      );
 
    const unregister: DesktopBackendPool["Service"]["unregister"] = (id) =>
      Effect.gen(function* () {
        if (id === DesktopBackendManager.PRIMARY_INSTANCE_ID) {
          return yield* new DesktopBackendPoolCannotUnregisterPrimaryError();
        }
        const done = yield* Deferred.make<void>();
        const action = yield* SynchronizedRef.modifyEffect(
          instancesRef,
          (
            current,
          ): Effect.Effect<
            readonly [UnregisterAction, ReadonlyMap<BackendInstanceId, RegisteredInstance>]
          > => {
            const entry = current.get(id);
            if (entry === undefined) {
              return Effect.succeed([{ _tag: "Absent" } as const, current] as const);
            }
            if (entry._tag === "Closing") {
              return Effect.succeed([
                { _tag: "Wait", done: entry.done } as const,
                current,
              ] as const);
            }
            const next = new Map(current);
            next.set(id, { _tag: "Closing", done });
            return Effect.succeed([
              { _tag: "Close", entry } as const,
              next as ReadonlyMap<BackendInstanceId, RegisteredInstance>,
            ] as const);
          },
        );
 
        if (action._tag === "Absent") return;
        if (action._tag === "Wait") {
          yield* Deferred.await(action.done);
          return;
        }
 
        const finish = SynchronizedRef.modifyEffect(instancesRef, (current) => {
          const closing = current.get(id);
          if (closing?._tag !== "Closing" || closing.done !== done) {
            return Effect.succeed([undefined, current] as const);
          }
          const next = new Map(current);
          next.delete(id);
          return Effect.succeed([
            undefined,
            next as ReadonlyMap<BackendInstanceId, RegisteredInstance>,
          ] as const);
        }).pipe(Effect.andThen(Deferred.succeed(done, undefined)), Effect.asVoid);
        yield* Option.match(action.entry.scope, {
          onNone: () => Effect.void,
          onSome: (scope) => Scope.close(scope, Exit.void).pipe(Effect.ignore),
        }).pipe(Effect.ensuring(finish));
Read this as: Each dynamically registered pool instance owns a child scope. Unregister closes that scope and removes the instance, while the primary id is explicitly protected from unregister.

SSH takes a different route. The preload asks main to discover hosts, ensure a target, bootstrap a bearer session, issue a WebSocket token, inspect session state, or disconnect. DesktopSshEnvironment delegates those operations to the SSH environment manager with desktop password prompts. A ready remote descriptor is a normal environment connection; it does not create a local DesktopBackendInstance.

4. Three file descriptors carry three different concerns

For the ordinary host-local primary, the child is run in Node mode and receives its JSON bootstrap envelope on fd3 (--bootstrap-fd 3). The envelope specifies desktop mode, noBrowser, port, home, bind host, bootstrap token, exposure settings, and the telemetry descriptors. It sets fd4 as parent-to-child host telemetry and fd5 as child-to-parent telemetry control.

fd4 starts with a hello and carries sampled Electron-main host/power/process data. fd5 carries control messages such as setDiagnosticsDemand: main tracks the demand per backend source, samples expensive Electron app metrics only while at least one source demands diagnostics, and removes demand when a control stream stops or a backend exits. It is a demand signal, not a general command channel and not a claim that the server can directly inspect every renderer or remote machine.

apps/desktop/src/backend/DesktopBackendManager.ts:451–526 ↗verbatim · typescript · 9a38b54e
  const onOutput = options.onOutput ?? (() => Effect.void);
  const bootstrapStream = Stream.encodeText(Stream.make(`${bootstrapJson}\n`));
  const additionalFds: Record<`fd${number}`, ChildProcess.AdditionalFdConfig> = {};
  if (options.bootstrapDelivery === "fd3") {
    additionalFds.fd3 = {
      type: "input",
      stream: bootstrapStream,
    };
    if (options.bootstrap.desktopTelemetryFd !== undefined) {
      additionalFds[`fd${options.bootstrap.desktopTelemetryFd}`] = {
        type: "input",
        stream: options.desktopTelemetryStream,
      };
    }
    if (options.bootstrap.desktopTelemetryControlFd !== undefined) {
      additionalFds[`fd${options.bootstrap.desktopTelemetryControlFd}`] = {
        type: "output",
      };
    }
  }
  const command = ChildProcess.make(options.executablePath, options.args, {
    cwd: options.cwd,
    env: options.env,
    extendEnv: options.extendEnv,
    // In Electron main, process.execPath points to the Electron binary.
    // Run the child in Node mode so this backend process does not become a GUI app instance.
    stdin: options.bootstrapDelivery === "stdin" ? bootstrapStream : "ignore",
    stdout: options.captureOutput ? "pipe" : "inherit",
    stderr: options.captureOutput ? "pipe" : "inherit",
    killSignal: "SIGTERM",
    forceKillAfter: DEFAULT_BACKEND_TERMINATE_GRACE,
    // wsl.exe drops additional file descriptors when forwarding to the Linux
    // side, so the WSL spawn path delivers the bootstrap envelope via stdin
    // (`--bootstrap-fd 0`) instead.
    ...(options.bootstrapDelivery === "fd3" ? { additionalFds } : {}),
  });
 
  const handle = yield* spawner.spawn(command).pipe(
    Effect.mapError(
      (cause) =>
        new BackendProcessSpawnError({
          executablePath: options.executablePath,
          entryPath: options.entryPath,
          cwd: options.cwd,
          httpBaseUrl: options.httpBaseUrl,
          cause,
        }),
    ),
  );
  const outputFibers: Array<Fiber.Fiber<void, never>> = [];
 
  yield* options.onStarted?.(handle.pid) ?? Effect.void;
  if (
    options.bootstrap.desktopTelemetryControlFd !== undefined &&
    options.onDesktopTelemetryControl !== undefined
  ) {
    const controlFd = options.bootstrap.desktopTelemetryControlFd;
    const handleControl = options.onDesktopTelemetryControl;
    yield* handle.getOutputFd(controlFd).pipe(
      Stream.decodeText(),
      Stream.splitLines,
      Stream.filter((line) => line.trim().length > 0),
      Stream.runForEach((line) =>
        decodeDesktopTelemetryControlLine(line).pipe(
          Effect.flatMap(handleControl),
          Effect.catchCause((cause) =>
            logBackendProcessWarning("ignored invalid desktop telemetry control message", {
              fd: controlFd,
              cause: Cause.pretty(cause),
            }),
          ),
        ),
      ),
      Effect.catchCause((cause) =>
        logBackendProcessWarning("desktop telemetry control stream stopped", {
          fd: controlFd,
Read this as: The host-local process receives bootstrap and telemetry through input descriptors and exposes diagnostics demand through one output descriptor. WSL swaps bootstrap delivery to stdin and does not inherit the extra channels.

WSL is the explicit exception. wsl.exe drops extra file descriptors across the Windows-to-Linux handoff, so the WSL child uses stdin (--bootstrap-fd 0) for the bootstrap envelope. The packaged Windows sidecar is also not passed to Linux; resource telemetry is unavailable for that WSL backend configuration. The fd3/fd4/ fd5 diagram therefore describes the primary local spawn, not a universal property of every environment.

5. Readiness opens the main window; preview guests stay in main

Starting a child is not readiness. The manager repeatedly probes its configured HTTP URL while the child remains alive. On success it resets that instance’s restart attempt and invokes the instance callback. For the primary, the pool passes that URL to the window service, which handles the main-window-ready transition. A slow WSL cold boot gets another probe interval rather than making the primary window wait.

apps/desktop/src/backend/DesktopBackendManager.ts:566–591 ↗verbatim · typescript · cb8c4181
  // Probe readiness in a loop while the backend process is still alive
  // instead of giving up after the first budget. A slow cold boot (the
  // WSL bundle loading across /mnt/c, or a first launch right after an
  // update) can exceed the initial readiness budget while the backend is
  // about to come up moments later; a one-shot probe left the app stuck
  // on "Connecting to WSL…" forever even though the backend kept running
  // and became healthy. Each round gets a fresh budget, and the forked
  // loop is torn down with the run scope once the child exits.
  const probeReadiness = Effect.fn("desktop.backendProcess.probeReadiness")(() =>
    waitForHttpReady({
      executablePath: options.executablePath,
      entryPath: options.entryPath,
      cwd: options.cwd,
      httpBaseUrl: options.httpBaseUrl,
      timeout: options.readinessTimeout ?? DEFAULT_BACKEND_READINESS_TIMEOUT,
    }).pipe(
      Effect.flatMap(() => options.onReady?.() ?? Effect.void),
      Effect.as(true),
      Effect.catchTags({
        BackendReadinessTimeoutError: (error) =>
          (options.onReadinessFailure?.(error) ?? Effect.void).pipe(Effect.as(false)),
      }),
    ),
  );
 
  yield* probeReadiness().pipe(Effect.repeat({ while: (ready) => !ready }), Effect.forkScoped);
Read this as: Readiness is an HTTP proof repeated with fresh budgets while the child is alive. A successful probe invokes the instance callback; a timeout records failure and allows another round.

The desktop preview is another useful boundary test. The renderer can request tab, navigation, zoom, screenshot, picking, recording, and automation operations through the curated preload API. Electron main’s preview manager validates and owns the guest Chromium WebContents. Server preview state can coordinate a tab record, but neither a remote client nor the page renderer thereby owns a browser process.

6. Menus, updates, and termination are stateful platform edges

Main configures the application menu and update service after Electron becomes ready; the renderer receives menu actions and update state through subscription-shaped preload calls and requests update checks/download/install through named IPC methods. Those UI messages are not the updater itself. The lifecycle code makes an important exception for updater-controlled quit: preventing the following before-quit can break Electron’s native install/relaunch sequence, especially on macOS.

For ordinary quit, the lifecycle marks the app as quitting, requests desktop shutdown, waits for the program’s finalization route, and then allows Electron to quit. The finalizer lists every pool instance and stops them concurrently, so WSL is not left to an OS hard kill. Individual backend stops cancel restart work, request SIGTERM with a grace period, and clear readiness. A child that exits unexpectedly while still desired schedules its own restart; that is intentionally different from the explicit shutdown path. On macOS, closing every window does not itself quit; activation can re-open the window if shutdown has not begun.

apps/desktop/src/app/DesktopLifecycle.ts:198–227 ↗verbatim · typescript · cc836843
    let updaterQuitAllowed = false;
    yield* electronTheme.onUpdated(() => {
      void runEffect(
        desktopWindow.syncAppearance.pipe(Effect.withSpan("desktop.lifecycle.themeUpdated")),
      );
    });
    yield* electronApp.onBeforeQuitForUpdate(() => {
      // Electron's updater owns the remaining quit/install/relaunch sequence.
      // Cancelling the following app "before-quit" event breaks that sequence,
      // most visibly on macOS where the native updater performs the relaunch.
      updaterQuitAllowed = true;
      void runEffect(
        logLifecycleInfo("allowing updater-controlled quit").pipe(
          Effect.withSpan("desktop.lifecycle.beforeQuitForUpdate"),
        ),
      );
    });
    yield* electronApp.on("before-quit", (event: Electron.Event) => {
      handleBeforeQuit(
        event,
        runEffect,
        () => quitAllowed || updaterQuitAllowed,
        () => {
          quitAllowed = true;
        },
      );
    });
    yield* electronApp.on("activate", () => {
      void runEffect(
        Effect.gen(function* () {
Read this as: Updater-controlled quit is granted its own pass through before-quit. Ordinary quit still takes the lifecycle hold, while activation and window-close behavior remain platform-aware.
apps/desktop/src/app/DesktopApp.ts:295–315 ↗verbatim · typescript · cd58d3b5
    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)),
    );
Read this as: The desktop program finalizer enumerates the current pool and stops every local instance concurrently before marking shutdown complete.

Work the process map

Use the lab to choose the host-local primary, Windows + WSL delivery, or SSH gateway, then advance from boot to readiness to shutdown. The labels intentionally keep transport and authority separate: a remote environment can be fully connected without being a locally pooled process.

Interactive process lab · Chapter 32

Choose a delivery path, then advance its lifecycle

The same renderer can observe different transports; native and process authority stays in Electron main.

Delivery path

Host-local primary · Boot

  1. 1Boot
  2. 2Readiness
  3. 3Shutdown

Boot

Host-local primary

Main resolves the primary configuration, starts the host-local child, and probes its HTTP endpoint.

Process
Electron main → primary local server child on macOS, Linux, or Windows → renderer window
Transport
fd3 bootstrap; fd4 host telemetry; fd5 diagnostics-demand control; HTTP / WebSocket
Authority
Electron main selects the child configuration and owns native APIs; renderer receives only curated IPC.
Static process map
PathProcess / transportBoundary
Host-local primaryElectron main → primary local server child on macOS, Linux, or Windows → renderer window
fd3 bootstrap; fd4 host telemetry; fd5 diagnostics-demand control; HTTP / WebSocket
Electron main selects the child configuration and owns native APIs; renderer receives only curated IPC.
Windows + WSLElectron main → either a WSL-only primary, or a Windows primary plus optional WSL secondary → renderer connections
A Windows host-local primary uses fd3/fd4/fd5. WSL receives bootstrap on stdin because wsl.exe drops forwarded extra descriptors.
The pool owns these local child instances. WSL is a distinct Linux environment, not a replacement for the SSH gateway.
SSH gatewayElectron main → SSH environment manager → remote t3 process / tunnel → renderer connection
Curated renderer IPC asks main to discover, ensure, bootstrap, or disconnect an SSH environment; the remote endpoint is reached through the SSH path.
SSH is a remote environment gateway owned by the desktop SSH service. It is not an entry in the local backend-process pool.

The durable rule is compact: renderer asks; preload narrows; main owns native effects; local pool owns local children; WSL is optional local Linux delivery; SSH owns a remote connection path. Keeping those nouns separate prevents both unsafe desktop APIs and misleading topology diagrams.

T3
Source-locked editionRead against fa219001d · 23 Aug 2026
Book search

Find a concept, module, or source path

Type two or more characters.