fix: v0.6.1 修复回复/推理期间偶发崩溃 — 浏览器回退窗口竞态 + 崩溃可观测性
【根因(实证归因,非猜测)】 分析 userData/logs/main.log 全部 33 次启动会话,定位 3 处异常终止点 (07-25 ×2 / 08-22 ×1,启动标记前无 Database closed)。三处 100% 共享 同一模式:web_search 并行抓取 → 多个 web_fetch 同时进入浏览器回退 → 共享单例 BrowserWindowManager 中后到 open() 销毁前一个正在加载/执行 JS 的窗口。关键统计:56 次浏览器回退中 ERR_ABORTED(并发互毁的直接 证据)仅 3 次,而这 3 次恰好全部对应 3 个崩溃点;无并发销毁的 53 次 回退从未崩溃 —— 触发条件完全收敛。 缺陷链(三层叠加): 1. browserFetch 直接 open/evaluate 共享单例,无跨调用序列化 — 并发 回退互相销毁窗口(ERR_ABORTED / "Object has been destroyed") 2. destroy() 对仍在使用中的 partition fire-and-forget clearStorageData/clearCache,与紧随其后的新窗口创建并发 — 原生存储层竞态(崩溃引爆点) 3. ensureReady 检查与实际 executeJavaScript/loadURL 之间存在竞态窗口; loadURLWithTimeout 的 Race 落败方 rejection 无人处理 【修复(browser-window-manager.ts + web-fetch.ts + browser.ts)】 - 新增 fetchPageText:排队版页面抓取,串行化完整 open→等待→evaluate 序列(与 open 共用单一操作链,destroy 只会在链上发生,跨链互毁彻底 消除);web_fetch 浏览器回退改走此入口 - open() 拆分 openInternal(链内直调);open 与 fetchPageText 共用 单一串行链,排队不分死锁 - destroy() 移除 session 存储清理(终态清理迁移至 close(),await 执行, 不再与窗口创建并发) - safeWebContents() 即时校验替代 racy 的 ensureReady;evaluate/extract/ screenshot/click/type/scroll/waitForSelector 全部加固,消除对已销毁 webContents 的调用 - loadURLWithTimeout 落败方 rejection 兜底(防 unhandledRejection) - cleanupBrowser/cleanup/close 异步化适配(main.ts 退出链路 await) 【崩溃可观测性(此前崩溃无迹可查 — 日志无声截断)】 - process.on(uncaughtException/unhandledRejection) → [FATAL] 落盘 - app.on(render-process-gone/child-process-gone) → [FATAL] 落盘 - WindowManager: 每窗口 render-process-gone 日志 + 自动 reload 自愈 (渲染进程 OOM/崩溃不再白屏卡死,可自动恢复) 【验证】 - lint 0/0;typecheck 双工程 0 错误;test:electron 252/252;build 通过
This commit is contained in:
+26
-1
@@ -95,6 +95,26 @@ import { HealthChecker, SLOMonitor } from './utils/slo';
|
||||
log.transports.file.level = 'info';
|
||||
log.transports.console.level = 'debug';
|
||||
|
||||
// ===== 崩溃可观测性(此前崩溃无迹可查 — main.log 只会无声中断) =====
|
||||
// 三个历史崩溃点(07-25 ×2 / 08-22 ×1)的日志均无声截断,无任何错误线索。
|
||||
// 以下钩子确保任何致命异常/进程级崩溃都先落盘再终止,供事后归因。
|
||||
process.on('uncaughtException', (err) => {
|
||||
log.error('[FATAL] uncaughtException:', err?.stack ?? err);
|
||||
});
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
log.error('[FATAL] unhandledRejection:', reason instanceof Error ? reason.stack : reason);
|
||||
});
|
||||
app.on('render-process-gone', (_event, webContents, details) => {
|
||||
log.error(
|
||||
`[FATAL] render-process-gone: wcId=${webContents.id} reason=${details.reason} exitCode=${details.exitCode}`,
|
||||
);
|
||||
});
|
||||
app.on('child-process-gone', (_event, details) => {
|
||||
log.error(
|
||||
`[FATAL] child-process-gone: type=${details.type} reason=${details.reason} exitCode=${details.exitCode ?? 'n/a'}`,
|
||||
);
|
||||
});
|
||||
|
||||
// ===== 工作空间路径独立存储(解决 DB 在 workspace 内的鸡生蛋问题)=====
|
||||
const WORKSPACE_CONFIG_FILE = join(app.getPath('userData'), 'workspace-config.json');
|
||||
|
||||
@@ -776,7 +796,12 @@ async function initialize(): Promise<void> {
|
||||
} catch (err) {
|
||||
log.error('[Shutdown] MCP shutdown failed:', err);
|
||||
}
|
||||
cleanupBrowser();
|
||||
// 崩溃修复: cleanupBrowser 现为异步(close 含 session 存储终态清理)
|
||||
try {
|
||||
await cleanupBrowser();
|
||||
} catch (err) {
|
||||
log.error('[Shutdown] Browser cleanup failed:', err);
|
||||
}
|
||||
|
||||
// v0.3.18 修复: 等待进行中的记忆固化任务完成,避免应用退出导致记忆丢失
|
||||
if (memoryConsolidator.isRunning()) {
|
||||
|
||||
Reference in New Issue
Block a user