fix(desktop): guard destroyed recovery windows (#37406)

This commit is contained in:
Luke Parker 2026-07-17 11:22:18 +10:00 committed by GitHub
commit 4436678f7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 38 additions and 11 deletions

View file

@ -33,6 +33,7 @@ import {
type SidecarListener, type SidecarListener,
} from "./server" } from "./server"
import { setupAutoUpdater, showUpdaterDialog } from "./updater" import { setupAutoUpdater, showUpdaterDialog } from "./updater"
import { safeWebContentsURL } from "./window-state"
import { import {
getLastFocusedWindow, getLastFocusedWindow,
registerRendererProtocol, registerRendererProtocol,
@ -233,7 +234,7 @@ const main = Effect.gen(function* () {
}) })
app.on("render-process-gone", (_event, webContents, details) => { app.on("render-process-gone", (_event, webContents, details) => {
writeLog("window", "app render process gone", { url: webContents.getURL(), details }, "error") writeLog("window", "app render process gone", { url: safeWebContentsURL(webContents), details }, "error")
}) })
setRelaunchHandler(() => { setRelaunchHandler(() => {

View file

@ -1,5 +1,6 @@
import type { BrowserWindow } from "electron" import type { BrowserWindow } from "electron"
import { write as writeLog } from "./logging" import { write as writeLog } from "./logging"
import { safeWindowURL } from "./window-state"
const sampleInterval = 1000 const sampleInterval = 1000
const samplePeriod = 15000 const samplePeriod = 15000
@ -46,7 +47,7 @@ export function createUnresponsiveSampler(win: BrowserWindow, name: string) {
const message = [ const message = [
"renderer unresponsive samples", "renderer unresponsive samples",
`Window: ${name}`, `Window: ${name}`,
`URL: ${win.isDestroyed() ? "<destroyed>" : win.webContents.getURL()}`, `URL: ${safeWindowURL(win)}`,
...entries.map((entry) => `<${entry[1]}> ${entry[0]}`), ...entries.map((entry) => `<${entry[1]}> ${entry[0]}`),
`Total Samples: ${total}`, `Total Samples: ${total}`,
].join("\n") ].join("\n")

View file

@ -0,0 +1,29 @@
export const destroyedWindowURL = "<destroyed>"
type WebContentsURLState = {
isDestroyed(): boolean
getURL(): string
}
type WindowURLState = {
isDestroyed(): boolean
readonly webContents: WebContentsURLState
}
export function safeWebContentsURL(webContents: WebContentsURLState) {
try {
if (webContents.isDestroyed()) return destroyedWindowURL
return webContents.getURL()
} catch {
return destroyedWindowURL
}
}
export function safeWindowURL(win: WindowURLState) {
try {
if (win.isDestroyed()) return destroyedWindowURL
return safeWebContentsURL(win.webContents)
} catch {
return destroyedWindowURL
}
}

View file

@ -13,6 +13,7 @@ import { getStore, removeStoreFile } from "./store"
import { PINCH_ZOOM_ENABLED_KEY, WINDOW_IDS_KEY } from "./store-keys" import { PINCH_ZOOM_ENABLED_KEY, WINDOW_IDS_KEY } from "./store-keys"
import { createUnresponsiveSampler } from "./unresponsive" import { createUnresponsiveSampler } from "./unresponsive"
import { createWindowRegistry } from "./window-registry" import { createWindowRegistry } from "./window-registry"
import { safeWindowURL } from "./window-state"
const root = dirname(fileURLToPath(import.meta.url)) const root = dirname(fileURLToPath(import.meta.url))
const rendererRoot = join(root, "../renderer") const rendererRoot = join(root, "../renderer")
@ -364,7 +365,7 @@ function wireWindowRecovery(win: BrowserWindow, name: string) {
errorCode, errorCode,
errorDescription, errorDescription,
validatedURL, validatedURL,
currentURL: win.webContents.getURL(), currentURL: safeWindowURL(win),
isMainFrame, isMainFrame,
}, },
"error", "error",
@ -386,12 +387,7 @@ function wireWindowRecovery(win: BrowserWindow, name: string) {
}) })
win.webContents.on("render-process-gone", (_event, details) => { win.webContents.on("render-process-gone", (_event, details) => {
sampler.stopAndFlush() sampler.stopAndFlush()
writeLog( writeLog("window", "renderer process gone", { window: name, currentURL: safeWindowURL(win), details }, "error")
"window",
"renderer process gone",
{ window: name, currentURL: win.webContents.getURL(), details },
"error",
)
void show( void show(
"OpenCode window terminated unexpectedly", "OpenCode window terminated unexpectedly",
[`Window: ${name}`, `Reason: ${details.reason}`, `Code: ${details.exitCode ?? "<unknown>"}`].join("\n"), [`Window: ${name}`, `Reason: ${details.reason}`, `Code: ${details.exitCode ?? "<unknown>"}`].join("\n"),
@ -399,12 +395,12 @@ function wireWindowRecovery(win: BrowserWindow, name: string) {
) )
}) })
win.on("unresponsive", () => { win.on("unresponsive", () => {
writeLog("window", "renderer unresponsive", { window: name, currentURL: win.webContents.getURL() }, "error") writeLog("window", "renderer unresponsive", { window: name, currentURL: safeWindowURL(win) }, "error")
sampler.start() sampler.start()
void show("OpenCode is not responding", "You can relaunch the app, open the logs, or keep waiting.", true) void show("OpenCode is not responding", "You can relaunch the app, open the logs, or keep waiting.", true)
}) })
win.on("responsive", () => { win.on("responsive", () => {
writeLog("window", "renderer responsive", { window: name, currentURL: win.webContents.getURL() }, "error") writeLog("window", "renderer responsive", { window: name, currentURL: safeWindowURL(win) }, "error")
sampler.stopAndFlush() sampler.stopAndFlush()
}) })
win.webContents.on("console-message", (_event, level, message, line, sourceId) => { win.webContents.on("console-message", (_event, level, message, line, sourceId) => {