Electron WindowSessionEndEvent 解析:在 Windows 上拦截系统关机、注销与重启会话
在 Electron 构建的 Windows 桌面应用中,用户点下“关机”“注销”或“重启”的那一刻,操作系统会向每个前台进程发出会话结束通知。WindowSessionEndEvent 是 Electron 将这一 Win32 系统级消息暴露给 JS 层的事件对象:通过监听 query-session-end 和 session-end 两个事件并读取其 reasons 属性,开发者可以判断系统即将执行哪种会话操作,并在 query-session-end 阶段决定是否推迟关机以保护用户数据。读完本文,你将掌握 WindowSessionEndEvent 的字段构成、两种事件的触发时序与拦截边界,以及从 Win32 WM_QUERYENDSESSION/WM_ENDSESSION 消息到 JS 事件对象的完整底层链路。
WindowSessionEndEvent 对象结构
WindowSessionEndEvent 继承自 Node.js 的 Event 对象(结构定义),它只有一个自有属性:
reasonsstring[]—— 本次会话结束的原因列表。取值可以是'shutdown'、'close-app'、'critical'或'logoff'。
这个对象会作为事件参数传递给 BaseWindow / BrowserWindow 上的两个 Windows 专属事件:
query-session-endWindows —— 当系统即将因关机、重启或注销而结束会话时触发。此时调用event.preventDefault()可以推迟系统关机。Electron 文档同时提醒:一般应尊重用户结束会话的选择,只有在会话结束会让用户面临数据丢失风险时才应拦截。session-endWindows —— 会话确定要结束时触发。一旦该事件触发,就没有任何办法阻止会话结束。
两个事件的完整说明见 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。作为 NativeWindowObserver 的 BaseWindow API 包装对象实现了这两个回调。
第三步:构造事件对象并发射到 JS 层
在 electron_api_base_window.cc 中,两个回调分别构造 WindowSessionEndEvent 对象——创建一个 gin_helper::internal::Event 包装器,用 gin::Dictionary 把 reasons 数组写入事件对象,然后发射对应事件:
OnWindowQueryEndSession发射query-session-end,并通过event->GetDefaultPrevented()检查 JS 是否调用了preventDefault(),若是则把*prevent_default置为true,最终使WM_QUERYENDSESSION应答为FALSE;OnWindowEndSession发射session-end,此时已无任何拦截能力。
第四步:WM_ENDSESSION 与进程快速退出
WM_ENDSESSION 的处理逻辑见 源码,有两个值得注意的细节:
- 每个窗口都会收到
session-end。由于 Windows 会在该消息返回后立即杀死进程及其所有子进程,Electron 会先把当前进程中的全部NativeWindow弱指针收集起来,逐一调用NotifyWindowEndSession(reasons),保证主进程里每个窗口的监听者都能收到通知; - 处理完毕后立即终止进程。除非应用已经在自己的会话结束处理逻辑中发起了
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-end 与 session-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 平台构建更健壮的关机/注销数据保护逻辑。相关入口:
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00