import { app, dialog } from "electron" import fs from "fs" import path from "path" import os from "os" import logger from "./main" import errorHandler, { ErrorDetail } from "./main-error" /** * 崩溃报告接口 */ export interface CrashReport { timestamp: string error: ErrorDetail systemInfo: { platform: string release: string arch: string totalMemory: number freeMemory: number uptime: number } appInfo: { version: string name: string path: string argv: string[] } } /** * 崩溃处理配置 */ export interface CrashHandlerOptions { crashReportDir?: string maxReports?: number showDialog?: boolean } /** * 默认崩溃处理配置 */ const DEFAULT_OPTIONS: CrashHandlerOptions = { maxReports: 10, showDialog: true, } /** * 崩溃处理类 */ export class CrashHandler { private static instance: CrashHandler private options: CrashHandlerOptions private crashReportDir: string private initialized: boolean = false private startTime: number = Date.now() // private normalShutdown: boolean = false /** * 获取单例实例 */ public static getInstance(): CrashHandler { if (!CrashHandler.instance) { CrashHandler.instance = new CrashHandler() } return CrashHandler.instance } /** * 构造函数 */ private constructor() { this.options = { ...DEFAULT_OPTIONS } this.crashReportDir = path.join(app.getPath("userData"), "crash-reports") } /** * 初始化崩溃处理器 */ public init(options?: CrashHandlerOptions): void { if (this.initialized) { return } this.options = { ...this.options, ...options } if (options?.crashReportDir) { this.crashReportDir = options.crashReportDir } // 确保崩溃报告目录存在 if (!fs.existsSync(this.crashReportDir)) { fs.mkdirSync(this.crashReportDir, { recursive: true }) } // 检查上次是否崩溃 this.checkPreviousCrash() // 记录应用启动时间 this.startTime = Date.now() this.saveStartupMarker() // 设置全局未捕获异常处理器 this.setupGlobalHandlers() // 设置应用退出处理 app.on("before-quit", () => { // this.normalShutdown = true this.clearStartupMarker() }) this.initialized = true logger.info("crash-handler", "Crash handler initialized") } /** * 设置全局未捕获异常处理器 */ private setupGlobalHandlers(): void { // 增强现有的错误处理器 const originalCaptureError = errorHandler.captureError.bind(errorHandler) errorHandler.captureError = (error: any, componentInfo?: string, additionalInfo?: Record) => { // 调用原始方法记录错误 originalCaptureError(error, componentInfo, additionalInfo) // 对于严重错误,生成崩溃报告 if (error instanceof Error && error.stack) { this.generateCrashReport(error, componentInfo, additionalInfo) } } // 捕获未处理的Promise异常 process.on("unhandledRejection", (reason, promise) => { logger.error("crash-handler", `Unhandled Promise Rejection: ${reason}`) this.generateCrashReport(reason, "unhandledRejection", { promise: String(promise) }) }) // 捕获未捕获的异常 process.on("uncaughtException", error => { logger.error("crash-handler", `Uncaught Exception: ${error.message}`) this.generateCrashReport(error, "uncaughtException") // 显示错误对话框 if (this.options.showDialog) { this.showCrashDialog(error) } }) } /** * 生成崩溃报告 */ private generateCrashReport(error: any, componentInfo?: string, additionalInfo?: Record): void { try { // 格式化错误信息 const errorDetail = this.formatError(error, componentInfo, additionalInfo) // 创建崩溃报告 const crashReport: CrashReport = { timestamp: new Date().toISOString(), error: errorDetail, systemInfo: { platform: os.platform(), release: os.release(), arch: os.arch(), totalMemory: os.totalmem(), freeMemory: os.freemem(), uptime: os.uptime(), }, appInfo: { version: app.getVersion(), name: app.getName(), path: app.getAppPath(), argv: process.argv, }, } // 保存崩溃报告 this.saveCrashReport(crashReport) } catch (e) { logger.error("crash-handler", `Failed to generate crash report: ${e}`) } } /** * 格式化错误信息 */ private formatError(error: any, componentInfo?: string, additionalInfo?: Record): ErrorDetail { // 基本错误信息 const errorDetail: ErrorDetail = { message: "", timestamp: new Date().toISOString(), type: "Unknown", } // 处理不同类型的错误 if (error instanceof Error) { errorDetail.message = error.message errorDetail.type = error.name || error.constructor.name errorDetail.stack = error.stack } else if (typeof error === "string") { errorDetail.message = error errorDetail.type = "String" } else if (error === null) { errorDetail.message = "Null error received" errorDetail.type = "Null" } else if (error === undefined) { errorDetail.message = "Undefined error received" errorDetail.type = "Undefined" } else if (typeof error === "object") { try { errorDetail.message = error.message || JSON.stringify(error) errorDetail.type = "Object" errorDetail.additionalInfo = { ...error } } catch (e) { errorDetail.message = "Unserializable error object" errorDetail.type = "Unserializable" } } else { try { errorDetail.message = String(error) errorDetail.type = typeof error } catch (e) { errorDetail.message = "Error converting to string" errorDetail.type = "Unknown" } } // 添加组件信息 if (componentInfo) { errorDetail.componentInfo = componentInfo } // 添加额外信息 if (additionalInfo) { errorDetail.additionalInfo = { ...errorDetail.additionalInfo, ...additionalInfo, } } return errorDetail } /** * 保存崩溃报告 */ private saveCrashReport(report: CrashReport): void { try { // 生成唯一的崩溃报告文件名 const timestamp = new Date().toISOString().replace(/[:.]/g, "-") const filename = `crash-${timestamp}.json` const filepath = path.join(this.crashReportDir, filename) // 写入崩溃报告 fs.writeFileSync(filepath, JSON.stringify(report, null, 2)) logger.info("crash-handler", `Crash report saved: ${filepath}`) // 清理旧的崩溃报告 this.cleanupOldReports() } catch (e) { logger.error("crash-handler", `Failed to save crash report: ${e}`) } } /** * 清理旧的崩溃报告 */ private cleanupOldReports(): void { try { // 获取所有崩溃报告文件 const files = fs .readdirSync(this.crashReportDir) .filter(file => file.startsWith("crash-") && file.endsWith(".json")) .map(file => ({ name: file, path: path.join(this.crashReportDir, file), time: fs.statSync(path.join(this.crashReportDir, file)).mtime.getTime(), })) .sort((a, b) => b.time - a.time) // 按时间降序排序 // 删除超出最大数量的旧报告 if (files.length > this.options.maxReports!) { const filesToDelete = files.slice(this.options.maxReports!) filesToDelete.forEach(file => { fs.unlinkSync(file.path) logger.debug("crash-handler", `Deleted old crash report: ${file.name}`) }) } } catch (e) { logger.error("crash-handler", `Failed to cleanup old reports: ${e}`) } } /** * 保存启动标记 */ private saveStartupMarker(): void { try { const markerPath = path.join(this.crashReportDir, "startup-marker.json") const marker = { startTime: this.startTime, pid: process.pid, } fs.writeFileSync(markerPath, JSON.stringify(marker)) } catch (e) { logger.error("crash-handler", `Failed to save startup marker: ${e}`) } } /** * 清除启动标记 */ private clearStartupMarker(): void { try { const markerPath = path.join(this.crashReportDir, "startup-marker.json") if (fs.existsSync(markerPath)) { fs.unlinkSync(markerPath) } } catch (e) { logger.error("crash-handler", `Failed to clear startup marker: ${e}`) } } /** * 检查上次是否崩溃 */ private checkPreviousCrash(): boolean { try { const markerPath = path.join(this.crashReportDir, "startup-marker.json") // 如果存在启动标记,说明上次可能崩溃了 if (fs.existsSync(markerPath)) { const markerData = JSON.parse(fs.readFileSync(markerPath, "utf8")) const lastStartTime = markerData.startTime const lastPid = markerData.pid logger.warn( "crash-handler", `Found previous startup marker. App may have crashed. Last PID: ${lastPid}, Last start time: ${new Date(lastStartTime).toISOString()}`, ) // 查找最近的崩溃报告 const recentCrash = this.getRecentCrashReport() // 显示崩溃恢复对话框 if (recentCrash && this.options.showDialog) { app.whenReady().then(() => { this.showRecoveryDialog(recentCrash) }) } // 清除旧的启动标记 fs.unlinkSync(markerPath) return true } } catch (e) { logger.error("crash-handler", `Failed to check previous crash: ${e}`) } return false } /** * 获取最近的崩溃报告 */ private getRecentCrashReport(): CrashReport | null { try { // 获取所有崩溃报告文件 const files = fs .readdirSync(this.crashReportDir) .filter(file => file.startsWith("crash-") && file.endsWith(".json")) .map(file => ({ name: file, path: path.join(this.crashReportDir, file), time: fs.statSync(path.join(this.crashReportDir, file)).mtime.getTime(), })) .sort((a, b) => b.time - a.time) // 按时间降序排序 // 读取最近的崩溃报告 if (files.length > 0) { const recentFile = files[0] const reportData = fs.readFileSync(recentFile.path, "utf8") return JSON.parse(reportData) as CrashReport } } catch (e) { logger.error("crash-handler", `Failed to get recent crash report: ${e}`) } return null } /** * 显示崩溃对话框 */ private showCrashDialog(error: Error): void { try { const options = { type: "error" as const, title: "应用崩溃", message: "应用遇到了一个严重错误,即将关闭", detail: `错误信息: ${error.message}\n\n堆栈信息: ${error.stack}\n\n崩溃报告已保存,应用将在您点击确定后关闭。`, buttons: ["确定"], defaultId: 0, } dialog.showMessageBoxSync(options) // 强制退出应用 setTimeout(() => { app.exit(1) }, 1000) } catch (e) { logger.error("crash-handler", `Failed to show crash dialog: ${e}`) app.exit(1) } } /** * 显示恢复对话框 */ private showRecoveryDialog(crashReport: CrashReport): void { try { const crashTime = new Date(crashReport.timestamp).toLocaleString() const errorMessage = crashReport.error.message const errorType = crashReport.error.type const options = { type: "warning" as const, title: "应用恢复", message: "应用上次异常退出", detail: `应用在 ${crashTime} 因 ${errorType} 错误崩溃: ${errorMessage}\n\n崩溃报告已保存,您可以继续使用应用或联系开发者报告此问题。`, buttons: ["继续", "查看详情"], defaultId: 0, } const response = dialog.showMessageBoxSync(options) // 如果用户选择查看详情 if (response === 1) { // 显示详细的崩溃报告 this.showDetailedCrashInfo(crashReport) } } catch (e) { logger.error("crash-handler", `Failed to show recovery dialog: ${e}`) } } /** * 显示详细的崩溃信息 */ private showDetailedCrashInfo(crashReport: CrashReport): void { try { const options = { type: "info" as const, title: "崩溃详情", message: "应用崩溃详细信息", detail: JSON.stringify(crashReport, null, 2), buttons: ["关闭"], defaultId: 0, } dialog.showMessageBoxSync(options) } catch (e) { logger.error("crash-handler", `Failed to show detailed crash info: ${e}`) } } /** * 设置崩溃处理选项 */ public setOptions(options: Partial): void { this.options = { ...this.options, ...options } } /** * 获取当前选项 */ public getOptions(): CrashHandlerOptions { return { ...this.options } } } // 创建默认实例 const crashHandler = CrashHandler.getInstance() export default crashHandler export { crashHandler }