| 1 | import { app, dialog, shell } from "electron"; |
| 2 | import { join } from "node:path"; |
| 3 | import { GRAPHICS_RECOVERY_ARG, type GraphicsSettingsStore } from "./graphics.js"; |
| 4 | import { GraphicsFaultRecord, GraphicsRecovery } from "./graphicsRecovery.js"; |
| 5 | import type { QuitSequencer } from "./lifecycle.js"; |
| 6 | import { errorText, type Logger } from "./log.js"; |
| 7 | |
| 8 | export async function confirmRecoveryDraftLoss(): Promise<boolean> { |
| 9 | const result = await dialog.showMessageBox({ |
| 10 | type: "warning", title: "Draft not saved / 草稿未保存", |
| 11 | message: "The unresponsive interface could not save your draft. Restart anyway? / 界面未能保存当前草稿,仍要重启吗?", |
| 12 | detail: "Unsaved draft changes may be lost. Saved conversations will be kept. / 未保存的草稿修改可能丢失,已保存的会话会保留。", |
| 13 | buttons: ["Restart anyway / 仍然重启", "Cancel / 取消"], defaultId: 1, cancelId: 1, noLink: true, |
| 14 | }); |
| 15 | return result.response === 0; |
| 16 | } |
| 17 | |
| 18 | export function installGraphicsRecovery(options: { |
| 19 | graphics: GraphicsSettingsStore; lifecycle: QuitSequencer; log: Logger; |
| 20 | logsDir: string; build: string; temporary: boolean; |
| 21 | }) { |
| 22 | const { graphics, lifecycle, log, logsDir } = options; |
| 23 | const record = new GraphicsFaultRecord(join(app.getPath("userData"), "graphics-fault.json"), options.build); |
| 24 | const previousFailure = record.pending(); |
| 25 | const stopping = () => lifecycle.currentPhase !== "idle"; |
| 26 | const error = (value: unknown) => log.warn(`graphics recovery: ${errorText(value)}`); |
| 27 | let actualAcceleration = graphics.current.startupEnabled; |
| 28 | let keepOffered = false; |
| 29 | const recovery = new GraphicsRecovery({ |
| 30 | stopping, accelerationEnabled: () => actualAcceleration, |
| 31 | record: fault => { |
| 32 | log.error(`graphics fault: ${JSON.stringify({ ...fault, requested: graphics.current.startupEnabled, actual: actualAcceleration })}`); |
| 33 | try { log.info(`graphics fault processes: ${JSON.stringify(app.getAppMetrics())}`); } catch (e) { error(e); } |
| 34 | record.record(fault); |
| 35 | }, |
| 36 | acknowledge: () => record.acknowledge(), error, |
| 37 | offer: async gpu => { |
| 38 | await app.whenReady(); |
| 39 | if (stopping()) return; |
| 40 | let response: number; |
| 41 | do { |
| 42 | const shownFault = record.checkpoint(); |
| 43 | const result = await dialog.showMessageBox({ |
| 44 | type: "warning", title: "Reasonix recovery / Reasonix 恢复", |
| 45 | message: gpu |
| 46 | ? "A graphics process failed. Try restarting without hardware acceleration. / 图形进程发生异常,可尝试关闭硬件加速后重启。" |
| 47 | : "The interface failed or stopped responding. / 界面发生异常或持续无响应。", |
| 48 | detail: "Restarting interrupts running tasks. Reasonix will try to save your draft first. / 重启会中断正在执行的任务,Reasonix 会先尝试保存草稿。", |
| 49 | buttons: [gpu ? "Restart in compatibility mode / 以兼容模式重启" : "Restart / 重启", "Open diagnostics / 查看诊断", "Not now / 暂不重启"], |
| 50 | defaultId: 2, cancelId: 2, noLink: true, |
| 51 | }); |
| 52 | response = result.response; |
| 53 | if (response !== 1) { |
| 54 | try { record.acknowledge(shownFault); } catch (e) { error(e); } |
| 55 | } |
| 56 | if (response === 1 && !stopping()) await shell.openPath(logsDir); |
| 57 | } while (response === 1 && !stopping()); |
| 58 | if (stopping()) return; |
| 59 | if (response !== 0) return; |
| 60 | const args = process.argv.slice(1).filter(arg => arg !== GRAPHICS_RECOVERY_ARG); |
| 61 | if (gpu || options.temporary) args.push(GRAPHICS_RECOVERY_ARG); |
| 62 | lifecycle.recoverRenderer(args); |
| 63 | }, |
| 64 | }); |
| 65 | app.on("child-process-gone", (_event, details) => { |
| 66 | if (details.type === "GPU") recovery.fault({ role: "GPU", reason: details.reason, exitCode: details.exitCode }); |
| 67 | }); |
| 68 | app.on("gpu-info-update", () => { |
| 69 | try { |
| 70 | actualAcceleration = app.isHardwareAccelerationEnabled(); |
| 71 | log.info(`graphics actual acceleration: ${actualAcceleration}`); |
| 72 | void app.getGPUInfo("basic").then(info => log.info(`graphics device: ${JSON.stringify(info)}`)).catch(error); |
| 73 | } catch (e) { error(e); } |
| 74 | }); |
| 75 | const timer = setInterval(() => recovery.tick(), 1000); |
| 76 | timer.unref(); |
| 77 | app.once("will-quit", () => clearInterval(timer)); |
| 78 | void app.whenReady().then(() => { |
| 79 | if (previousFailure && !options.temporary) recovery.offer(true); |
| 80 | }).catch(error); |
| 81 | return { |
| 82 | recovery, |
| 83 | healthy: () => { |
| 84 | recovery.healthy(); |
| 85 | if (!options.temporary || keepOffered || stopping() || recovery.isPrompting) return; |
| 86 | keepOffered = recovery.notice(async () => { |
| 87 | const shownFault = record.checkpoint(); |
| 88 | const result = await dialog.showMessageBox({ |
| 89 | type: "info", title: "Compatibility mode / 兼容模式", |
| 90 | message: "Reasonix started with hardware acceleration disabled. / Reasonix 已关闭硬件加速并成功启动。", |
| 91 | detail: "You can keep it disabled for future launches and change it later in Settings. This does not establish the cause of the failure. / 可将关闭状态用于后续启动,之后可在设置中修改。成功启动不代表已经确认故障原因。", |
| 92 | buttons: ["Keep disabled / 保持关闭", "This launch only / 仅本次关闭"], defaultId: 1, cancelId: 1, noLink: true, |
| 93 | }); |
| 94 | try { record.acknowledge(shownFault); } catch (e) { error(e); } |
| 95 | if (result.response === 0 && !stopping()) { |
| 96 | try { await graphics.setHardwareAcceleration(false); } |
| 97 | catch (e) { |
| 98 | error(e); |
| 99 | await dialog.showMessageBox({ |
| 100 | type: "error", title: "Setting not saved / 设置未保存", |
| 101 | message: "The preference could not be saved. Compatibility mode still applies to this launch. / 无法保存设置,本次启动仍保持兼容模式。", |
| 102 | buttons: ["OK / 确定"], noLink: true, |
| 103 | }); |
| 104 | } |
| 105 | } |
| 106 | }); |
| 107 | }, |
| 108 | }; |
| 109 | } |
| 110 |