Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
import {
getAppBranding,
getLocalEnvironmentBootstraps,
readLocalEnvironmentBootstraps,
getLocalEnvironmentBearerToken,
getSystemLocale,
getWindowFullscreenState,
Expand Down Expand Up @@ -97,6 +98,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handleSync(getSystemLocale);
yield* ipc.handleSync(getWindowFullscreenState);
yield* ipc.handleSync(getLocalEnvironmentBootstraps);
yield* ipc.handle(readLocalEnvironmentBootstraps);
yield* ipc.handleSync(getLocalEnvironmentEnabled);
yield* ipc.handle(setLocalEnvironmentEnabled);
yield* ipc.handle(getLocalEnvironmentBearerToken);
Expand Down
137 changes: 74 additions & 63 deletions apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,73 +90,84 @@ export const getWindowFullscreenState = DesktopIpc.makeSyncIpcMethod({
}),
});

export const getLocalEnvironmentBootstraps = DesktopIpc.makeSyncIpcMethod({
channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL,
result: Schema.Array(DesktopEnvironmentBootstrapSchema),
handler: Effect.fn("desktop.ipc.window.getLocalEnvironmentBootstraps")(function* () {
const pool = yield* DesktopBackendPool.DesktopBackendPool;
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
const instances = yield* pool.list;
const bootstraps: DesktopEnvironmentBootstrap[] = [];
for (const instance of instances) {
const isPrimary = instance.id === PRIMARY_LOCAL_ENVIRONMENT_ID;
const config = yield* instance.currentConfig;
const snapshot = yield* instance.snapshot;
// A secondary backend (e.g. a parallel WSL backend) that hasn't produced
// a config yet (mid-registration, before its first start cycle) or that
// is retrying a *transient* preflight failure (WSL VM still booting, a
// not-yet-built linux server entry) is not listening on a port. We
// surface it as a *pending* bootstrap (null endpoints, no token) so the
// renderer can show a "Connecting…" indicator while it retries — null
// endpoints keep the renderer from dialing the dead port, avoiding the
// needless /api/auth/bootstrap/bearer error cycles a real endpoint would
// trigger.
if (Option.isNone(config) || Option.isSome(config.value.preflightFailure)) {
// Skip the primary (same-origin, no "connecting" affordance) and skip a
// secondary whose preflight failed *fatally* (no node, wrong version,
// missing build tools): it has stopped retrying, so an indefinite
// "Connecting…" would be misleading — its error is surfaced by the
// WSL-state UI instead.
const fatalPreflight =
Option.isSome(config) &&
Option.isSome(config.value.preflightFailure) &&
config.value.preflightFailure.value.fatal;
const stoppedPreflight =
Option.isSome(config) &&
Option.isSome(config.value.preflightFailure) &&
(!snapshot.desiredRunning || !snapshot.restartScheduled);
if (isPrimary || fatalPreflight || stoppedPreflight) continue;
bootstraps.push({
id: instance.id,
label: yield* instance.label,
runningDistro: null,
httpBaseUrl: null,
wsBaseUrl: null,
});
continue;
}
const { bootstrap, httpBaseUrl } = config.value;
const runningDistro = config.value.runningDistro ?? null;
const readLocalEnvironmentBootstrapSnapshot = Effect.fn(
"desktop.ipc.window.getLocalEnvironmentBootstraps",
)(function* () {
const pool = yield* DesktopBackendPool.DesktopBackendPool;
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
const instances = yield* pool.list;
const bootstraps: DesktopEnvironmentBootstrap[] = [];
for (const instance of instances) {
const isPrimary = instance.id === PRIMARY_LOCAL_ENVIRONMENT_ID;
const config = yield* instance.currentConfig;
const snapshot = yield* instance.snapshot;
// A secondary backend (e.g. a parallel WSL backend) that hasn't produced
// a config yet (mid-registration, before its first start cycle) or that
// is retrying a *transient* preflight failure (WSL VM still booting, a
// not-yet-built linux server entry) is not listening on a port. We
// surface it as a *pending* bootstrap (null endpoints, no token) so the
// renderer can show a "Connecting…" indicator while it retries — null
// endpoints keep the renderer from dialing the dead port, avoiding the
// needless /api/auth/bootstrap/bearer error cycles a real endpoint would
// trigger.
if (Option.isNone(config) || Option.isSome(config.value.preflightFailure)) {
// Skip the primary (same-origin, no "connecting" affordance) and skip a
// secondary whose preflight failed *fatally* (no node, wrong version,
// missing build tools): it has stopped retrying, so an indefinite
// "Connecting…" would be misleading — its error is surfaced by the
// WSL-state UI instead.
const fatalPreflight =
Option.isSome(config) &&
Option.isSome(config.value.preflightFailure) &&
config.value.preflightFailure.value.fatal;
const stoppedPreflight =
Option.isSome(config) &&
Option.isSome(config.value.preflightFailure) &&
(!snapshot.desiredRunning || !snapshot.restartScheduled);
if (isPrimary || fatalPreflight || stoppedPreflight) continue;
bootstraps.push({
id: instance.id,
label: runningDistro === null ? yield* instance.label : `WSL (${runningDistro})`,
runningDistro,
httpBaseUrl: httpBaseUrl.href,
wsBaseUrl: toWebSocketBaseUrl(httpBaseUrl),
// A backend launched with the desktop secret accepts whichever token
// the secret derives for the current window, so hand out that one
// rather than the token frozen into its launch config. Every backend
// the desktop launches (primary, staged or mounted WSL runtime) is the
// server build bundled with this desktop, so it understands the secret.
...(bootstrap.desktopBootstrapSecret
? { bootstrapToken: yield* configuration.currentBootstrapToken }
: bootstrap.desktopBootstrapToken
? { bootstrapToken: bootstrap.desktopBootstrapToken }
: {}),
label: yield* instance.label,
runningDistro: null,
httpBaseUrl: null,
wsBaseUrl: null,
});
continue;
}
return bootstraps;
}),
const { bootstrap, httpBaseUrl } = config.value;
const runningDistro = config.value.runningDistro ?? null;
bootstraps.push({
id: instance.id,
label: runningDistro === null ? yield* instance.label : `WSL (${runningDistro})`,
runningDistro,
httpBaseUrl: httpBaseUrl.href,
wsBaseUrl: toWebSocketBaseUrl(httpBaseUrl),
// A backend launched with the desktop secret accepts whichever token
// the secret derives for the current window, so hand out that one
// rather than the token frozen into its launch config. Every backend
// the desktop launches (primary, staged or mounted WSL runtime) is the
// server build bundled with this desktop, so it understands the secret.
...(bootstrap.desktopBootstrapSecret
? { bootstrapToken: yield* configuration.currentBootstrapToken }
: bootstrap.desktopBootstrapToken
? { bootstrapToken: bootstrap.desktopBootstrapToken }
: {}),
});
}
return bootstraps;
});

export const getLocalEnvironmentBootstraps = DesktopIpc.makeSyncIpcMethod({
channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL,
result: Schema.Array(DesktopEnvironmentBootstrapSchema),
handler: readLocalEnvironmentBootstrapSnapshot,
});

export const readLocalEnvironmentBootstraps = DesktopIpc.makeIpcMethod({
channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL,
payload: Schema.Void,
result: Schema.Array(DesktopEnvironmentBootstrapSchema),
handler: readLocalEnvironmentBootstrapSnapshot,
});

// Pull the distro selection out of a backend instance id like
Expand Down
26 changes: 18 additions & 8 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { contextBridge, ipcRenderer, webFrame, webUtils } from "electron";

import * as IpcChannels from "./ipc/channels.ts";
import { mergeLegacyLocalStorage } from "./legacyLocalStorageMerge.ts";
import { createPreloadSnapshot } from "./preloadSnapshot.ts";

const SNAP_SHOT_EVENT_TYPES = new Set([
"requested",
Expand Down Expand Up @@ -76,6 +77,19 @@ function unwrapEnsureSshEnvironmentResult(result: unknown) {
return result as Awaited<ReturnType<DesktopBridge["ensureSshEnvironment"]>>;
}

const readBootstraps = (
result: unknown,
): ReturnType<DesktopBridge["getLocalEnvironmentBootstraps"]> =>
Array.isArray(result) ? result : [];
const localEnvironmentBootstraps = createPreloadSnapshot(
() => readBootstraps(ipcRenderer.sendSync(IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL)),
async () =>
readBootstraps(await ipcRenderer.invoke(IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL)),
);
// This setting changes only through a relaunch; repeated renderer reads do
// not need to block on the main process.
let localEnvironmentEnabled: boolean | undefined;

contextBridge.exposeInMainWorld("desktopBridge", {
getAppBranding: () => {
const result = ipcRenderer.sendSync(IpcChannels.GET_APP_BRANDING_CHANNEL);
Expand All @@ -102,17 +116,13 @@ contextBridge.exposeInMainWorld("desktopBridge", {
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"]>;
},
getLocalEnvironmentBootstraps: localEnvironmentBootstraps.read,
readLocalEnvironmentBootstraps: localEnvironmentBootstraps.refresh,
getLocalEnvironmentBearerToken: () =>
ipcRenderer.invoke(IpcChannels.GET_LOCAL_ENVIRONMENT_BEARER_TOKEN_CHANNEL),
getLocalEnvironmentEnabled: () =>
ipcRenderer.sendSync(IpcChannels.GET_LOCAL_ENVIRONMENT_ENABLED_CHANNEL) !== false,
(localEnvironmentEnabled ??=
ipcRenderer.sendSync(IpcChannels.GET_LOCAL_ENVIRONMENT_ENABLED_CHANNEL) !== false),
setLocalEnvironmentEnabled: (enabled) =>
ipcRenderer.invoke(IpcChannels.SET_LOCAL_ENVIRONMENT_ENABLED_CHANNEL, enabled),
getClientSettings: () => ipcRenderer.invoke(IpcChannels.GET_CLIENT_SETTINGS_CHANNEL),
Expand Down
62 changes: 62 additions & 0 deletions apps/desktop/src/preloadSnapshot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { expect, it } from "vite-plus/test";

import { createPreloadSnapshot } from "./preloadSnapshot.ts";

it("keeps reads responsive while the main process is busy and shares a refresh", async () => {
let synchronousReads = 0;
let asynchronousReads = 0;
let complete!: (value: string[]) => void;
const latest = new Promise<string[]>((resolve) => {
complete = resolve;
});
const snapshot = createPreloadSnapshot(
() => {
synchronousReads++;
return ["primary"];
},
() => {
asynchronousReads++;
return latest;
},
);

expect(snapshot.read()).toEqual(["primary"]);
const refresh = snapshot.refresh();
expect(snapshot.refresh()).toBe(refresh);
for (let read = 0; read < 1_000; read++) expect(snapshot.read()).toEqual(["primary"]);
expect(synchronousReads).toBe(1);
expect(asynchronousReads).toBe(1);

complete(["primary", "wsl"]);
await refresh;
expect(snapshot.read()).toEqual(["primary", "wsl"]);
expect(synchronousReads).toBe(1);
});

it("preserves the last successful topology after IPC fails and recovers on the next refresh", async () => {
let fail = true;
const snapshot = createPreloadSnapshot(
() => ["primary", "wsl"],
async () => {
if (fail) throw new Error("IPC unavailable");
return [];
},
);
expect(snapshot.read()).toEqual(["primary", "wsl"]);
await expect(snapshot.refresh()).rejects.toThrow("IPC unavailable");
expect(snapshot.read()).toEqual(["primary", "wsl"]);
fail = false;
await snapshot.refresh();
expect(snapshot.read()).toEqual([]);
});

it("uses an asynchronous startup result without a synchronous IPC read", async () => {
const snapshot = createPreloadSnapshot(
() => {
throw new Error("unexpected synchronous IPC");
},
async () => ["wsl"],
);
await snapshot.refresh();
expect(snapshot.read()).toEqual(["wsl"]);
});
24 changes: 24 additions & 0 deletions apps/desktop/src/preloadSnapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/** Keep synchronous startup consumers while refreshing through nonblocking IPC. */
export function createPreloadSnapshot<T>(readInitial: () => T, readLatest: () => Promise<T>) {
let snapshot: { readonly value: T } | undefined;
let pending: Promise<T> | undefined;

return {
read: (): T => {
snapshot ??= { value: readInitial() };
return snapshot.value;
},
refresh: (): Promise<T> => {
if (pending !== undefined) return pending;
pending = readLatest()
.then((value) => {
snapshot = { value };
return value;
})
.finally(() => {
pending = undefined;
});
return pending;
},
};
}
41 changes: 41 additions & 0 deletions apps/web/src/connection/desktopLocal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,47 @@ describe("desktop local connection identity", () => {
});

describe("desktop local topology reads", () => {
it("refreshes asynchronously without calling the synchronous bridge and preserves failures", async () => {
const secondary: DesktopEnvironmentBootstrap = {
id: "wsl:default",
label: "WSL",
runningDistro: "Ubuntu",
httpBaseUrl: "http://127.0.0.1:4000",
wsBaseUrl: "ws://127.0.0.1:4000",
bootstrapToken: "bootstrap-1",
};
let entries = [secondary];
let failure: Error | undefined;
const reader = createDesktopSecondaryBootstrapsReader(() => ({
getLocalEnvironmentBootstraps: () => {
throw new Error("synchronous IPC unavailable");
},
readLocalEnvironmentBootstraps: async () => {
if (failure !== undefined) throw failure;
return entries.map((entry) => ({ ...entry }));
},
}));
const connected = await reader.refresh();
expect(connected).toEqual({ _tag: "Success", bootstraps: [secondary] });
if (connected._tag !== "Success") throw new Error("expected connected topology");
expect(reader.readSnapshot()).toBe(connected.bootstraps);
expect(await reader.refresh()).toEqual(connected);
failure = new Error("main process unavailable");
expect(await reader.refresh()).toEqual({ _tag: "Failure", cause: failure });
expect(reader.readSnapshot()).toBe(connected.bootstraps);
failure = undefined;
entries = [];
expect(await reader.refresh()).toEqual({ _tag: "Success", bootstraps: [] });
expect(reader.readSnapshot()).toEqual([]);
});

it("refreshes through an older desktop's synchronous bridge", async () => {
const reader = createDesktopSecondaryBootstrapsReader(() => ({
getLocalEnvironmentBootstraps: () => [],
}));
expect(await reader.refresh()).toEqual({ _tag: "Success", bootstraps: [] });
});

it("reuses snapshots when polling returns fresh objects for the same topology", () => {
const secondary: DesktopEnvironmentBootstrap = {
id: "wsl:default",
Expand Down
Loading
Loading