From 9cec0aa61c4bd641d1a057c9608606bd2ca8345d Mon Sep 17 00:00:00 2001 From: Spencer Morris Date: Fri, 16 Oct 2026 12:25:28 -0400 Subject: [PATCH] fix(desktop): reload the main renderer after it is killed --- apps/desktop/src/window/DesktopWindow.test.ts | 41 +++++++++++++++++++ apps/desktop/src/window/DesktopWindow.ts | 2 + 2 files changed, 43 insertions(+) diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index d17b530a86dd1..8f8b8d4077743 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -1339,6 +1339,47 @@ describe("DesktopWindow", () => { }), ); + it.effect( + "recovers killed renderers without reloading indefinitely or reviving closed windows", + () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const layer = layerTest({ window: fakeWindow.window, createCount, mainWindow }); + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.createMain; + const onGone = fakeWindow.webContentsListeners.get("render-process-gone"); + const onLoaded = fakeWindow.webContentsListeners.get("did-finish-load"); + if (!onGone || !onLoaded) + return yield* Effect.die("renderer lifecycle listeners missing"); + + onGone({}, { reason: "clean-exit", exitCode: 0 }); + yield* TestClock.adjust(500); + assert.equal(fakeWindow.loadURL.mock.calls.length, 1); + + for (let attempt = 0; attempt < 4; attempt += 1) { + onGone({}, { reason: "killed", exitCode: 9 }); + yield* TestClock.adjust(500); + onLoaded(); + } + assert.equal(fakeWindow.loadURL.mock.calls.length, 4); + + yield* TestClock.adjust(60_000); + onGone({}, { reason: "killed", exitCode: 9 }); + yield* TestClock.adjust(500); + assert.equal(fakeWindow.loadURL.mock.calls.length, 5); + assert.equal(yield* Ref.get(createCount), 1); + + onGone({}, { reason: "killed", exitCode: 9 }); + fakeWindow.isDestroyed.mockReturnValue(true); + yield* TestClock.adjust(500); + assert.equal(fakeWindow.loadURL.mock.calls.length, 5); + }).pipe(Effect.provide(layer)); + }), + ); + it.effect("recovers when the development renderer is temporarily unreachable", () => Effect.gen(function* () { const fakeWindow = makeFakeBrowserWindow(); diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index d9f9805edab1a..2a39c15193e6f 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -756,9 +756,11 @@ export const make = Effect.gen(function* () { const recoverable = details.reason === "crashed" || details.reason === "oom" || + details.reason === "killed" || details.reason === "abnormal-exit"; // Recovery attempts are bounded so a renderer that dies immediately on // boot cannot reload-loop forever. + runFork( Effect.gen(function* () { const now = yield* Clock.currentTimeMillis;