退出清理
功能特性
- ✅ 系统关机/注销拦截
- ✅ 用户确认对话框
- ✅ 自动资源清理
- ✅ 防止重复清理
- ✅ 超时保护机制
- ✅ 跨平台支持(Windows 完全支持)
主要适用场景:
- 有未保存的数据或任务正在执行
- 需要在关机前进行清理操作
- 防止用户误操作导致应用异常退出
架构设计
核心类:AppCleanupHandler
文件位置: packages/main/src/handlers/app-cleanup-handler.js
职责:
- ✅ 统一管理应用退出时的资源清理
- ✅ 处理系统关机/注销时的用户确认对话框
- ✅ 监听电源管理事件(挂起、恢复、关机)
- ✅ 防止重复清理,确保资源只释放一次
事件注册
文件位置: packages/main/src/apps/app-events.js
职责:
- 注册 Electron 应用事件
- 调用 AppCleanupHandler 的方法
- ❌ 不包含业务逻辑
配置说明
配置文件位置
resources/config/config.ini
配置项
ini
# 是否启用系统关机阻止功能
# true: 启用,关机时会弹出确认对话框
# false: 禁用,关机时直接退出应用
preventSystemShutdown="true"
# 关机确认对话框超时时间(毫秒)
# 如果用户在指定时间内未响应,将自动允许关机
# 建议值:5000-15000(5-15秒)
shutdownConfirmTimeout="10000"工作原理
事件监听
应用程序监听两种关机事件:
session-end 事件(主要方式,Windows 平台)
- 当 Windows 系统发送关机/注销信号时触发
- 这是最可靠的关机检测方式
- 支持的事件类型:
shutdown: 系统关机logoff: 用户注销
powerMonitor.shutdown 事件(辅助方式)
- 跨平台的电源管理 API
- 某些平台和场景下会触发
- 作为 session-end 的补充
处理流程
系统关机流程
系统关机/注销
↓
session-end 事件
↓
AppCleanupHandler.handleSessionEnd()
↓
检查 preventSystemShutdown 配置
↓
是否防止?
│
├─ 否 → 直接允许关机
│
└─ 是 → event.preventDefault()
↓
显示确认对话框
↓
用户选择?
│
├─ 继续关机 → app.quit()
│ ↓
│ before-quit
│ ↓
│ performCleanup()
│ ↓
│ process.exit(0)
│
└─ 取消关机 → 返回应用继续运行正常退出流程
用户操作(关闭窗口/退出应用)
↓
app.quit()
↓
before-quit 事件
↓
AppCleanupHandler.performCleanup()
↓
设置 global.isAppQuitting = true
↓
执行清理操作
↓
process.exit(0)确认对话框
对话框包含:
- 标题:系统关机确认
- 消息:系统即将关机/注销
- 详细信息:说明当前状态和操作选项
- 按钮:
- 继续关机:保存数据并退出应用
- 取消关机:阻止系统关机
超时机制
为防止阻塞系统关机过程,设置了超时保护:
- 默认 10 秒超时
- 超时后自动执行清理并允许关机
- 可通过
shutdownConfirmTimeout配置调整
清理操作
应用退出前自动执行以下清理流程:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 设置全局退出标志 | global.isAppQuitting = true |
| 2 | 注销全局快捷键 | 释放系统快捷键资源 |
| 3 | 记录退出信息 | 记录应用退出日志 |
| 4 | 关闭本地服务器 | 停止 HTTP 服务器 |
| 5 | 清理 IPC 处理器 | 移除所有 IPC 监听器 |
| 6 | 清理错误捕获 | 关闭错误捕获系统 |
| 7 | 停止日志清理 | 停止日志定时任务 |
| 8 | 强制退出进程 | process.exit(0) |
使用示例
示例 1:启用关机阻止
ini
preventSystemShutdown="true"
shutdownConfirmTimeout="10000"当用户关机时:
- 弹出确认对话框
- 用户选择"继续关机" → 应用清理资源后退出
- 用户选择"取消关机" → 系统关机被阻止
示例 2:禁用关机阻止
ini
preventSystemShutdown="false"当用户关机时:
- 应用直接退出,执行标准清理流程
- 不显示任何确认对话框
示例 3:自定义超时时间
ini
preventSystemShutdown="true"
shutdownConfirmTimeout="15000" # 15秒超时代码实现
核心类:AppCleanupHandler
1. 类结构
javascript
class AppCleanupHandler {
constructor() {
this.isShutdownConfirmed = false; // 关机确认标记
this.isCleanupPerformed = false; // 清理执行标记
}
/**
* 获取单例实例
* @returns {AppCleanupHandler}
*/
static getInstance() {
if (!AppCleanupHandler.instance) {
AppCleanupHandler.instance = new AppCleanupHandler();
}
return AppCleanupHandler.instance;
}
/**
* 执行清理操作
*/
async performCleanup() {
/* ... */
}
/**
* 处理系统会话结束事件
* @param {Event} event - 系统事件
* @param {string} reason - 结束原因(shutdown/logoff)
*/
handleSessionEnd(event, reason) {
/* ... */
}
/**
* 显示关机确认对话框
* @param {string} reason - 关机原因
*/
showShutdownConfirmDialog(reason) {
/* ... */
}
/**
* 注册电源监控事件
*/
registerPowerMonitorEvents() {
/* ... */
}
}2. 清理操作
javascript
async performCleanup() {
// ✅ 防止重复清理
if (this.isCleanupPerformed) {
logger.info('清理操作已执行,跳过重复清理');
return;
}
try {
logger.info('执行应用退出前清理操作');
this.isCleanupPerformed = true;
// 1️⃣ 设置全局退出标志
global.isAppQuitting = true;
// 2️⃣ 注销全局快捷键
ShortcutHandler.unregisterCustomShortcut();
// 3️⃣ 记录应用退出信息
loggerAppExitInfo();
// 4️⃣ 关闭本地服务器
const localServer = LocalServer.getInstance();
await localServer.stop();
// 5️⃣ 清理 IPC 处理器
IpcManager.removeHandlers();
// 6️⃣ 清理错误捕获系统
const errorManager = ErrorCaptureManager.getInstance();
errorManager.cleanup();
// 7️⃣ 停止日志清理定时任务
LoggerClean.getInstance().stopScheduledCleanup();
logger.info('清理操作完成,准备强制退出');
// 8️⃣ 强制退出进程
// 使用 setImmediate 确保异步操作(如日志写入)完成
setImmediate(() => {
process.exit(0); // ✅ 正常退出
});
} catch (error) {
logger.error('清理操作失败:', error);
// ⚠️ 清理失败也要退出,避免进程挂起
setImmediate(() => {
process.exit(1); // ❌ 异常退出
});
}
}3. 处理系统会话结束
javascript
handleSessionEnd(event, reason) {
logger.info(`检测到系统会话结束事件: ${reason}`);
const preventShutdown = getConfigValue('preventSystemShutdown', false);
if (preventShutdown && !this.isShutdownConfirmed) {
// 阻止默认行为
event.preventDefault();
this.showShutdownConfirmDialog(reason);
} else {
logger.info('允许系统会话结束');
}
}4. 显示确认对话框
javascript
showShutdownConfirmDialog(reason) {
logger.warn(`检测到系统${reason === 'shutdown' ? '关机' : '注销'}事件`);
// 获取超时配置
const timeout = getConfigValue('shutdownConfirmTimeout', 10000);
// 创建定时器,超时后自动允许关机
const timeoutId = setTimeout(() => {
logger.info('关机确认超时,允许系统关机');
this.isShutdownConfirmed = true;
app.quit();
}, timeout);
// 显示同步对话框
const choice = dialog.showMessageBoxSync({
type: 'warning',
buttons: ['继续关机', '取消关机'],
defaultId: 0,
cancelId: 1,
title: '系统关机确认',
message: `系统即将${reason === 'shutdown' ? '关机' : '注销'}`,
detail: '应用程序正在运行中,是否允许继续?',
noLink: true,
});
clearTimeout(timeoutId);
if (choice === 0) {
// 用户选择继续关机
logger.info('用户确认关机,准备退出应用');
this.isShutdownConfirmed = true;
app.quit(); // 触发 before-quit 事件
} else {
// 用户取消关机
logger.info('用户取消关机操作');
this.isShutdownConfirmed = false;
}
}事件注册(app-events.js)
javascript
const AppCleanupHandler = require('../handlers/app-cleanup-handler');
function registerAppEvents() {
// ✅ 获取应用清理处理器单例实例
const cleanupHandler = AppCleanupHandler.getInstance();
// 📌 应用退出时清理
app.on('before-quit', async () => {
logger.info('应用程序触发 before-quit 事件');
await cleanupHandler.performCleanup();
});
// 📌 Windows 系统会话结束事件(关机/注销)
app.on('session-end', (event, reason) => {
cleanupHandler.handleSessionEnd(event, reason);
});
// 📌 注册电源监控事件
cleanupHandler.registerPowerMonitorEvents();
}平台支持
Windows
- ✅ 完全支持
session-end事件可靠触发- 支持关机和注销检测
- 可以有效阻止系统关机(需要用户确认)
macOS
- ⚠️ 部分支持
powerMonitor.shutdown事件可能触发- macOS 系统对阻止关机有更严格的限制
- 建议使用
before-quit事件进行清理
Linux
- ⚠️ 部分支持
- 依赖桌面环境的实现
- 某些发行版和桌面环境可能不支持
- 建议测试后使用
注意事项
1. 不要无限期阻止关机
- 系统可能会强制关机(通常几秒钟后)
- 应快速完成清理操作
- 设置合理的超时时间
2. 用户体验考虑
- 对话框文案要清晰明了
- 提供明确的操作选项
- 避免频繁弹出确认对话框
3. 数据保存
- 在
before-quit事件中保存关键数据 - 不要依赖关机阻止来保存数据
- 实现自动保存机制
4. 测试建议
- 在真实关机场景下测试
- 测试不同的关机方式(开始菜单、Alt+F4、命令行等)
- 测试超时机制是否正常工作
5. 日志记录
所有关机相关事件都会记录到日志:
javascript
logger.info('检测到系统会话结束事件: shutdown');
logger.warn('检测到系统关机事件');
logger.info('用户确认关机,执行清理后退出');
logger.info('用户取消关机操作');常见问题
Q1: 为什么有时候阻止不了关机?
原因:
- 系统强制关机(按住电源键、强制重启等)
- 超时时间已到,自动允许关机
- 系统没有给应用足够的响应时间
解决方案:
- 使用合理的超时时间(5-15秒)
- 实现快速清理逻辑
- 定期自动保存数据
Q2: 对话框没有弹出?
检查:
- 配置文件中
preventSystemShutdown是否为true - 查看日志是否有相关事件记录
- 确认运行平台(主要支持 Windows)
Q3: 如何扩展自定义逻辑?
可以在 AppCleanupHandler 类中添加自定义方法:
javascript
class AppCleanupHandler {
// ...
showShutdownConfirmDialog(reason) {
// 检查是否有未保存的数据
const hasUnsavedData = this.checkUnsavedData();
if (hasUnsavedData) {
// 显示特殊提示
const choice = dialog.showMessageBoxSync({
type: 'warning',
buttons: ['保存并关机', '不保存直接关机', '取消关机'],
message: '您有未保存的数据',
detail: '请选择如何处理未保存的数据',
});
if (choice === 0) {
// 保存数据后关机
await this.saveAllData();
this.isShutdownConfirmed = true;
app.quit();
} else if (choice === 1) {
// 不保存直接关机
this.isShutdownConfirmed = true;
app.quit();
}
// choice === 2 时取消关机,不做任何操作
return;
}
// 原有逻辑
// ...
}
checkUnsavedData() {
// 自定义检查逻辑
return false;
}
async saveAllData() {
// 自定义保存逻辑
}
}Q4: 如何在渲染进程中检测关机?
通过 IPC 通信:
主进程:
javascript
app.on('session-end', (event, reason) => {
// 通知所有渲染进程
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send('system-shutdown', reason);
});
});渲染进程:
javascript
window.electron.on('system-shutdown', (reason) => {
console.log('系统即将关机:', reason);
// 执行渲染进程的清理操作
});最佳实践
1. 快速响应
javascript
// ✅ 推荐:快速清理
async performCleanup() {
// 并行执行独立的清理任务
await Promise.all([
LocalServer.getInstance().stop(),
IpcManager.removeHandlers(),
ErrorCaptureManager.getInstance().cleanup(),
]);
// 立即退出
setImmediate(() => process.exit(0));
}
// ❌ 不推荐:耗时操作
async performCleanup() {
await heavyDatabaseOperation(); // 太慢,阻塞关机
await uploadLargeFile(); // 不适合在退出时执行
}2. 使用单例模式
javascript
// ✅ 推荐:通过静态方法获取实例
const cleanupHandler = AppCleanupHandler.getInstance();
// ❌ 不推荐:多次创建实例
const handler1 = new AppCleanupHandler();
const handler2 = new AppCleanupHandler();3. 直接使用 process.exit()
javascript
// ✅ 推荐:清理后直接退出
setImmediate(() => {
process.exit(0);
});
// ❌ 不推荐:使用 app.quit() 可能导致重复清理
app.quit();4. 日志完整性
javascript
// 记录完整的关机流程
logger.info('检测到关机事件');
logger.info('显示确认对话框');
logger.info('用户选择结果:', choice);
logger.info('执行清理操作');
logger.info('应用退出完成');