Skip to content

退出清理

功能特性

  • ✅ 系统关机/注销拦截
  • ✅ 用户确认对话框
  • ✅ 自动资源清理
  • ✅ 防止重复清理
  • ✅ 超时保护机制
  • ✅ 跨平台支持(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"

工作原理

事件监听

应用程序监听两种关机事件:

  1. session-end 事件(主要方式,Windows 平台)

    • 当 Windows 系统发送关机/注销信号时触发
    • 这是最可靠的关机检测方式
    • 支持的事件类型:
      • shutdown: 系统关机
      • logoff: 用户注销
  2. 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"

当用户关机时:

  1. 弹出确认对话框
  2. 用户选择"继续关机" → 应用清理资源后退出
  3. 用户选择"取消关机" → 系统关机被阻止

示例 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: 对话框没有弹出?

检查:

  1. 配置文件中 preventSystemShutdown 是否为 true
  2. 查看日志是否有相关事件记录
  3. 确认运行平台(主要支持 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('应用退出完成');

参考资料

基于 MIT 许可发布