Browse Source
添加崩溃处理模块 `crash-handler`,用于捕获未处理的异常和未捕获的 Promise 拒绝,生成崩溃报告,并在崩溃时显示错误对话框。该模块还支持保存崩溃日志、清理旧报告以及在应用恢复时显示恢复对话框。此功能提高了应用的稳定性和可维护性,帮助开发者更好地诊断和解决崩溃问题。feat/icon
7 changed files with 506 additions and 0 deletions
@ -0,0 +1,486 @@ |
|||
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<string, any>) => { |
|||
// 调用原始方法记录错误
|
|||
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<string, any>): 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<string, any>): 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<CrashHandlerOptions>): void { |
|||
this.options = { ...this.options, ...options } |
|||
} |
|||
|
|||
/** |
|||
* 获取当前选项 |
|||
*/ |
|||
public getOptions(): CrashHandlerOptions { |
|||
return { ...this.options } |
|||
} |
|||
} |
|||
|
|||
// 创建默认实例
|
|||
const crashHandler = CrashHandler.getInstance() |
|||
|
|||
export default crashHandler |
|||
export { crashHandler } |
Loading…
Reference in new issue