Skip to content
Draft
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
11 changes: 10 additions & 1 deletion src/gaia/apps/webui/main-safety-net.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,20 @@ function installSafetyNet({ logPath, dialogModule, appModule, homedirFn }) {

appModule.on("child-process-gone", (_event, details) => {
const reason = details && details.reason;
const type = details && details.type;
// Ignore expected terminations during shutdown so the crash dialog
// doesn't flash on a clean quit.
if (reason === "clean-exit" || reason === "killed") return;
// A GPU-process crash is recoverable: Chromium relaunches the GPU process
// and, after repeated failures, falls back to software rendering. Treating
// it as fatal killed the whole app in GPU-less environments (Windows
// Sandbox, headless VMs). Log and let Chromium recover instead.
if (type === "GPU") {
appendLog(logPath, `[${new Date().toISOString()}] GPU_CRASH reason=${reason} (non-fatal; Chromium will fall back to software rendering)`);
return;
}
fatal(new Error(
`child-process-gone: type=${details && details.type} reason=${reason}`
`child-process-gone: type=${type} reason=${reason}`
));
});

Expand Down
39 changes: 39 additions & 0 deletions tests/electron/test_main_error_handling.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,45 @@ describe("installSafetyNet", () => {
exitSpy.mockRestore();
});

// ── Test 9b: GPU child-process crash is non-fatal (issue #1800) ────────────
// GPU crashes are recoverable — Chromium falls back to software rendering.
// The app must NOT exit, so GPU-less envs (Windows Sandbox/VM) keep running.

test("GPU child-process-gone does not call fatal/exit", () => {
const { installSafetyNet } = require(SAFETY_NET_PATH);
const dialog = mockDialog();
const app = mockApp(false);
const exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {});

installSafetyNet({ logPath, dialogModule: dialog, appModule: app });
app.emit("child-process-gone", {}, { type: "GPU", reason: "crashed" });

expect(exitSpy).not.toHaveBeenCalled();
expect(dialog.showErrorBox).not.toHaveBeenCalled();
expect(dialog.showMessageBoxSync).not.toHaveBeenCalled();
// The crash is still recorded for forensics.
expect(fs.readFileSync(logPath, "utf8")).toContain("GPU_CRASH");

exitSpy.mockRestore();
});

// ── Test 9c: non-GPU child-process crash stays fatal ──────────────────────
// A utility/plugin child dying unexpectedly is still treated as fatal.

test("non-GPU child-process-gone calls fatal/exit", () => {
const { installSafetyNet } = require(SAFETY_NET_PATH);
const dialog = mockDialog();
const app = mockApp(false);
const exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {});

installSafetyNet({ logPath, dialogModule: dialog, appModule: app });
app.emit("child-process-gone", {}, { type: "Utility", reason: "crashed" });

expect(exitSpy).toHaveBeenCalled();

exitSpy.mockRestore();
});

// ── Test 10: fatal handler writes to log before showing dialog ─────────────
// If dialog.showErrorBox itself crashes, the log must already have the entry.

Expand Down