mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
fix(gui): 修「长时间挂后台后打开白屏」—— 补 renderer/GPU 崩溃自愈
## 实测故障 GUI 在后台挂很久(数小时)后,从托盘打开是**白屏**,且不会自愈 —— 重启进程才恢复。 本机现场证据:GPU 进程启动于 20:27:56,而主进程是 16:03:29 —— GPU 进程在主进程运行 4 小时后重启过一次,正是长时间后台后 GPU context 被回收的实证。 ## 原因链(三个洞叠加) 1. **没有任何崩溃监听**。全代码搜不到 render-process-gone / unresponsive / child-process-gone。renderer 被回收后全程无感知。 2. **窗口「可见」不等于「有内容」**。renderer 挂掉后窗口句柄仍在、 isVisible() 返回 true,而 showMainWindow() 只判 `if (!isVisible()) show()` ⇒ show() 被跳过 ⇒ 托盘点一下开出白窗口, 永远不会好。 3. **ready-to-show 用了 once**。once 只等第一次首帧;窗口被系统回收后 重建、或 GPU 重启后重新 load,那些路径都需要再 show 一次, once 让它们静默失败。 仓库历史上有过「白屏修复(惰性Tray)」提交,但那只修了**首次**显示时机, 没管**长期后台后**。 ## 修复 - render-process-gone → reload 自愈;reason==='oom' 时**不** reload (内存不足时反复重载只会立刻再崩一次,把机器拖死) - child-process-gone 且 type==='GPU' → reload 渲染进程 (无边框窗口在GPU 重启后常停在上一帧/全白,与 renderer 崩溃表现重合 但成因不同;不重建窗口,避免丢窗口位置与尺寸) - showMainWindow() 加 webContents.isCrashed() 判定,为真则先 reload - ready-to-show: once → on - 通知点击路径改走 showMainWindow(),不再自己调 show() (原来的直接 show() 绕过健康检查,同样能开出白窗口) ## 判据(cmd/gui/window-liveness.test.mjs) 9 条,文本扫描(复现需「挂后台数小时 + renderer 被回收」,依赖驱动行为, CI 不可靠)。覆盖:render-process-gone 监听、崩溃后有 reload、 OOM 不重载、GPU 崩溃监听、isCrashed 判定、判定后重载、 ready-to-show 用 on、无绕过健康检查的直接 show、通知点击走 showMainWindow。 变异测试验证非假红:删掉 isCrashed 分支 ⇒ 对应两条转红。 排障中判据自身报过 2 次假红,根因都写进注释: - sliceFunction 从 match 结果换成字符串后,残留的 showFn[0] 取到首字符 - 注释里也写着 isCrashed(),纯字符串匹配命中注释而非代码
This commit is contained in:
105
cmd/gui/main.js
105
cmd/gui/main.js
@ -442,16 +442,60 @@ function createWindow(show = true) {
|
||||
});
|
||||
|
||||
// 静默时不显示;非静默时在首帧就绪后显示,避免白屏闪现。
|
||||
if (show) {
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
try {
|
||||
//
|
||||
// ★ 用 on 而不是 once(2026-10-05 修白屏):once 只等一次,首帧之后
|
||||
// 监听就没了。而窗口可能被系统回收后重建、或GPU 重启后重新 load,
|
||||
// 那时都需要再show 一次 —— once 的写法让那些路径静默失败。
|
||||
mainWindow.on("ready-to-show", () => {
|
||||
if (!show) return;
|
||||
try {
|
||||
if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.isVisible()) {
|
||||
mainWindow.show();
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
mainWindow.loadFile(path.join(__dirname, "renderer", "index.html"));
|
||||
|
||||
// ★ renderer 崩溃自愈(2026-10-05 修「长时间挂后台后打开白屏」)。
|
||||
//
|
||||
// 现象:GUI 在后台挂很久(数小时)后,从托盘打开是**白屏**,且不会自愈。
|
||||
//
|
||||
// 原因链:
|
||||
// 长时间后台 → Chromium 回收 renderer 的 GPU context(或渲染进程
|
||||
// 本身被判定 hung 杀掉)→ 窗口对象仍在、isVisible() 仍返回 true,
|
||||
// 但内容是空的 → showMainWindow() 的 `if (!isVisible()) show()` 被跳过
|
||||
// → 托盘点一下,窗口出现但是白的,永远不会好。
|
||||
//
|
||||
// 之前全代码没有任何 render-process-gone / unresponsive 监听,所以这条
|
||||
// 路径完全无感知。仓库历史上有过「白屏修复(惰性Tray)」提交,但那只修了
|
||||
// **首次**显示时机,没管**长期后台后**。
|
||||
mainWindow.webContents.on("render-process-gone", (e, details) => {
|
||||
console.error(
|
||||
"[window] renderer gone: " + (details && details.reason) +
|
||||
" (exitCode=" + (details && details.exitCode) + ")",
|
||||
);
|
||||
// OOM 之外的都重建;OOM(reason==="oom")重建只会立刻再崩一次,
|
||||
// 只记录日志避免反复重启把机器拖死。
|
||||
if (details && details.reason === "oom") return;
|
||||
try {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[window] reload after crash failed: " + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// renderer 活着但无响应(主线程被长任务卡死):此时页面还在,
|
||||
// 只是不响应。给一次刷新机会,不自动刷(误判会让正在用的用户丢状态)。
|
||||
mainWindow.webContents.on("unresponsive", () => {
|
||||
console.error("[window] renderer unresponsive");
|
||||
});
|
||||
mainWindow.webContents.on("responsive", () => {
|
||||
console.log("[window] renderer responsive again");
|
||||
});
|
||||
|
||||
if (process.argv.includes("--dev")) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
@ -3377,9 +3421,30 @@ function initTray() {
|
||||
function showMainWindow() {
|
||||
try {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
// ★ 健康检查(2026-10-05 修白屏):仅靠 isVisible() 不够。
|
||||
//
|
||||
// 长时间后台后 renderer 可能已被回收 —— 窗口句柄还在、isVisible()
|
||||
// 仍为 true,但内容是空的。此时原逻辑会跳过 show(),从托盘打开
|
||||
// 就是一片白,而且永远不会自愈。
|
||||
//
|
||||
// 两个判据:
|
||||
// isCrashed() —— 渲染进程已死,必须重载才能出内容;
|
||||
// isLoading() —— 还在加载,等ready-to-show 就行,不重载。
|
||||
const wc = mainWindow.webContents;
|
||||
if (wc.isCrashed()) {
|
||||
console.log("[window] renderer crashed, reloading before show");
|
||||
try {
|
||||
wc.reload();
|
||||
} catch (e) {}
|
||||
}
|
||||
// 窗口是静默创建时已处于「未显示」态,这里正常显示即可。
|
||||
// 用 showInactive + focus 两步:showInactive 不抢焦点地显示,
|
||||
// focus 再把焦点给过来(纯 show 在部分 Win 版本上会导致
|
||||
// 无边框窗口不获得焦点,用户还得再点一下)。
|
||||
if (!mainWindow.isVisible()) mainWindow.show();
|
||||
mainWindow.focus();
|
||||
try {
|
||||
mainWindow.focus();
|
||||
} catch (e) {}
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
@ -3508,6 +3573,25 @@ app.on("activate", () => {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// GPU 进程崩溃/重启后,无边框窗口(frame:false)在部分驱动上会停在
|
||||
// 上一帧不再重绘 —— 表现同样是「窗口在、内容不动或全白」。
|
||||
// `child-process-gone` 比 `render-process-gone` 覆盖面更广(含 GPU),
|
||||
// 这里只在 GPU 挂掉时重载渲染进程(不重建窗口,避免丢窗口位置/尺寸)。
|
||||
app.on("child-process-gone", (e, details) => {
|
||||
const type = details && details.type;
|
||||
const reason = details && details.reason;
|
||||
if (type !== "GPU") return;
|
||||
console.error("[window] GPU process gone: reason=" + reason);
|
||||
if (reason === "oom" || reason === "launch-failed") return;
|
||||
try {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[window] reload after GPU crash failed: " + err.message);
|
||||
}
|
||||
});
|
||||
// ============ 系统通知(Electron 原生)============
|
||||
//
|
||||
// 之前**完全没有**这个能力:主进程没引入 Notification、preload 没暴露接口、
|
||||
@ -3540,10 +3624,9 @@ ipcMain.handle("notify:show", (_, payload) => {
|
||||
urgency: p.urgent ? "critical" : "normal",
|
||||
});
|
||||
n.on("click", () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (!mainWindow.isVisible()) mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
// 走 showMainWindow 而非直接 show:它带renderer 健康检查,
|
||||
// 直接 show 在 renderer 已被回收时会开出一个白窗口。
|
||||
showMainWindow();
|
||||
try {
|
||||
mainWindow.webContents.send("notify:clicked", { id: id, msgKey: p.msgKey || "" });
|
||||
} catch (e) {}
|
||||
|
||||
Reference in New Issue
Block a user