首页
/ Electron WindowSessionEndEvent 解析:在 Windows 上拦截系统关机、注销与重启会话

Electron WindowSessionEndEvent 解析:在 Windows 上拦截系统关机、注销与重启会话

2026-09-06 17:43:45作者:冯梦姬Eddie

在 Electron 构建的 Windows 桌面应用中,用户点下“关机”“注销”或“重启”的那一刻,操作系统会向每个前台进程发出会话结束通知。WindowSessionEndEvent 是 Electron 将这一 Win32 系统级消息暴露给 JS 层的事件对象:通过监听 query-session-endsession-end 两个事件并读取其 reasons 属性,开发者可以判断系统即将执行哪种会话操作,并在 query-session-end 阶段决定是否推迟关机以保护用户数据。读完本文,你将掌握 WindowSessionEndEvent 的字段构成、两种事件的触发时序与拦截边界,以及从 Win32 WM_QUERYENDSESSION/WM_ENDSESSION 消息到 JS 事件对象的完整底层链路。

WindowSessionEndEvent 对象结构

WindowSessionEndEvent 继承自 Node.js 的 Event 对象(结构定义),它只有一个自有属性:

  • reasons string[] —— 本次会话结束的原因列表。取值可以是 'shutdown''close-app''critical''logoff'

这个对象会作为事件参数传递给 BaseWindow / BrowserWindow 上的两个 Windows 专属事件:

  • query-session-end Windows —— 当系统即将因关机、重启或注销而结束会话时触发。此时调用 event.preventDefault() 可以推迟系统关机。Electron 文档同时提醒:一般应尊重用户结束会话的选择,只有在会话结束会让用户面临数据丢失风险时才应拦截。
  • session-end Windows —— 会话确定要结束时触发。一旦该事件触发,就没有任何办法阻止会话结束。

两个事件的完整说明见 BaseWindow 事件文档BrowserWindow 事件文档

reasons 取值与 Win32 标志位的对应关系

reasons 数组并非随意约定,而是对 Win32 WM_ENDSESSION 消息低字参数中各标志位的直接翻译。从源码 EndSessionToStringVec 可以看到完整的映射逻辑:

reasons 来源 含义
'shutdown' 参数为 0(即没有任何标志位) 系统关机/重启。Windows 不提供区分关机与重启的手段,因此两种场景都会给出 'shutdown'
'close-app' ENDSESSION_CLOSEAPP 系统允许应用提示用户保存数据后退出
'critical' ENDSESSION_CRITICAL 系统处于关键状态,要求应用立即退出
'logoff' ENDSESSION_LOGOFF 当前用户正在注销

由于这些是独立的位标志,同一次会话结束中 reasons 可能同时包含多个值(例如关机且需要关闭应用时得到 ['close-app', 'critical'])。

底层消息处理链路

Electron 对会话结束的响应完全发生在 NativeWindowViewsWin 的窗口过程(WndProc)中,整条链路可以概括为“Win32 消息 → NativeWindow 通知 → NativeWindowObserver 回调 → JS 事件发射”四步。

第一步:拦截 WM_QUERYENDSESSION

窗口过程代码 中,当 Windows 发出 WM_QUERYENDSESSION 询问“你是否同意结束会话”时,Electron 的处理是:

case WM_QUERYENDSESSION: {
  bool prevent_default = false;
  std::vector<std::string> reasons = EndSessionToStringVec(l_param);
  NotifyWindowQueryEndSession(reasons, &prevent_default);
  // Result should be TRUE by default, otherwise WM_ENDSESSION will not be
  // fired in some cases
  *result = !prevent_default;
  return prevent_default;
}

关键点:返回给 Windows 的应答默认是 TRUE(允许结束会话),只有当 JS 层的 event.preventDefault() 被调用时才返回 FALSE 以推迟关机。注释中也指出,若默认不返回 TRUE,某些情况下 Windows 甚至不会发出后续的 WM_ENDSESSION

第二步:NativeWindow 观察者分发

NativeWindow 通过观察者列表把请求转发给所有注册的 NativeWindowObserver,见 NotifyWindowQueryEndSession / NotifyWindowEndSession 以及观察者接口声明 NativeWindowObserver。作为 NativeWindowObserverBaseWindow API 包装对象实现了这两个回调。

第三步:构造事件对象并发射到 JS 层

electron_api_base_window.cc 中,两个回调分别构造 WindowSessionEndEvent 对象——创建一个 gin_helper::internal::Event 包装器,用 gin::Dictionaryreasons 数组写入事件对象,然后发射对应事件:

  • OnWindowQueryEndSession 发射 query-session-end,并通过 event->GetDefaultPrevented() 检查 JS 是否调用了 preventDefault(),若是则把 *prevent_default 置为 true,最终使 WM_QUERYENDSESSION 应答为 FALSE
  • OnWindowEndSession 发射 session-end,此时已无任何拦截能力。

第四步:WM_ENDSESSION 与进程快速退出

WM_ENDSESSION 的处理逻辑见 源码,有两个值得注意的细节:

  1. 每个窗口都会收到 session-end。由于 Windows 会在该消息返回后立即杀死进程及其所有子进程,Electron 会先把当前进程中的全部 NativeWindow 弱指针收集起来,逐一调用 NotifyWindowEndSession(reasons),保证主进程里每个窗口的监听者都能收到通知;
  2. 处理完毕后立即终止进程。除非应用已经在自己的会话结束处理逻辑中发起了 app.quit()(通过 Browser::Get()->is_quitting() 判断),否则会直接调用 base::Process::TerminateCurrentProcessImmediately(0),避免在 Windows 拆除子进程期间残留。

实战示例:在会话结束前保存用户数据

结合上述时序,一个典型的 Windows 主进程处理方案如下:

const { BaseWindow, app } = require('electron')

const createWindow = () => {
  const win = new BaseWindow({
    width: 800,
    height: 600
  })

  // 第一阶段:系统询问是否结束会话,此时还可拦截
  win.on('query-session-end', (event) => {
    const reasons = event.reasons // ['shutdown'] 或 ['close-app', 'critical'] 等

    if (hasUnsavedChanges()) {
      // 推迟系统关机,给用户保存数据的机会
      event.preventDefault()
      askUserToSaveAndQuit()
    }
    // 不调用 preventDefault 时,允许系统继续关机/注销
  })

  // 第二阶段:会话确定结束,无法拦截,应快速落盘
  win.on('session-end', (event) => {
    console.log('Session ending with reasons:', event.reasons)
    flushPendingWrites() // 注意:OS 随后会立即终止本进程
  })
}

let unsaved = false
const hasUnsavedChanges = () => unsaved
const markDirty = () => { unsaved = true }
const markClean = () => { unsaved = false }

function askUserToSaveAndQuit () {
  // 例如:对话框确认后调用 win.close() 并清理状态
  unsaved = false
  app.quit()
}

使用时的几点约束:

  • 这两个事件只在 Windows 上触发,在 macOS/Linux 上监听不会收到任何事件;
  • query-session-end 是唯一的拦截窗口,且拦截只是推迟——Electron 文档明确建议仅在有数据丢失风险时使用;
  • session-end 触发后系统会很快杀掉进程,其中的回调应只做同步、低延迟的收尾工作(日志、写缓存),不要发起网络请求或异步任务。

已知限制:无法区分关机与重启

原文档特别指出:Windows 没有提供任何 API 来区分“关机”和“重启”,因此这两种场景下 reasons 都会是 ['shutdown']。如果你的应用需要在这两者之间做不同处理,Electron 目前无法帮助区分,只能依赖 reasons 中的其他标志(如 'logoff''critical')做更粗粒度的判断。

测试覆盖

spec/api-base-window-spec.ts 中,query-session-endsession-end 被纳入 Windows 专属的事件订阅测试集:

ifdescribe(process.platform === 'win32')('Windows events', () => {
  eventSubscriptionTests(['query-session-end', 'session-end'])
})

eventSubscriptionTests 验证了这两个事件均支持 on / once / off 的完整事件订阅生命周期,与文档声明的平台属性一致。

小结与扩展阅读

WindowSessionEndEvent 虽然结构简单——只有一个 reasons 字符串数组——但它背后串起了 Win32 会话结束协议与 Electron 跨进程架构的完整链路:从 WM_QUERYENDSESSION/WM_ENDSESSION 的位标志解析,到观察者模式的多窗口广播,再到 gin 层向 V8 事件对象的属性注入。理解它可以帮你在 Windows 平台构建更健壮的关机/注销数据保护逻辑。相关入口:

登录后查看全文
热门项目推荐
相关项目推荐