恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Cherry Studio 电源中枢 PowerService:Electron 电源事件、关机屏障与防睡眠机制完全指南
首页
资讯中心
/
Cherry Studio 电源中枢 PowerService:Electron 电源事件、关机屏障与防睡眠机制完全指南
Cherry Studio 电源中枢 PowerService:Electron 电源事件、关机屏障与防睡眠机制完全指南
发布时间:2026/9/12 4:09:08
Cherry Studio 电源中枢 PowerServiceElectron 电源事件、关机屏障与防睡眠机制完全指南【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio导读本文深入解析 Cherry Studio 主进程中统一的系统电源枢纽——PowerService位于 src/main/core/power/PowerService.ts。它是整个应用唯一直接接触 ElectronpowerMonitor/powerSaveBlocker的入口集中提供电源通知事件、跨平台关机清理屏障、基于引用计数的防睡眠机制与电平触发式电源查询。读完本文你将掌握这套电源管理的设计模式并能直接用文中 API 为自己的模块接入“机器挂起/恢复通知”“关机前清理”与“保持机器唤醒”三类能力。为什么需要“电源中枢”一个服务收编所有电源关注点在 Electron 应用中powerMonitor与powerSaveBlocker是全局性的系统级 API。若每个模块各自直接监听suspend、resume、shutdown事件或各自调用powerSaveBlocker.start()会带来三个典型问题监听与释放责任分散谁监听、谁移除、生命周期如何与模块同生共死难以统一管理事件重复消费macOS 上suspend/resume会被系统触发两次若无统一去重订阅方会收到重复通知防睡眠冲突多个模块同时想阻止睡眠时各自 start/stop 会互相覆盖无法收敛到“最后一个离开才释放”的语义。PowerService的解法是把这些关注点收编为一个生命周期管理的单例服务其余代码一律不直接触碰 Electron 电源 API而是通过服务的类型化接口间接使用。这与 Cherry Studio 主进程依赖注入Injectable装饰器 application.get(...)容器的设计一脉相承。其职责可以用一张表概括领域提供的能力通知事件类型化的Emitter→Event覆盖onSuspend/onResume/onLockScreen/onUnlockScreen/onPowerSourceChange。suspend/resume 与电源来源均基于内部状态去重macOS 会双触发见 Electron 上游 issue #24803lock/unlock 则直接透传关机屏障registerShutdownHandler(fn)返回Disposable。系统关机时处理器按序串行执行并受硬超时约束随后应用退出。跨平台实现macOS/Linux 使用powerMonitor的shutdown事件 preventDefault()Windows 使用paymoapp/electron-shutdown-handler原生插件防睡眠preventSleep(reason?)返回Disposable。基于引用计数ref-counted holds操作系统阻塞器prevent-app-suspension仅在“存在至少一个 hold 且用户已在app.power.prevent_sleep_when_busy中开启”时才激活。isPreventingSleep()报告生效状态查询getPowerPhase()/getPowerSource()/isOnBatteryPower()/getSystemIdleTime()/getSystemIdleState(thresholdSec)——采用电平触发语义迟到的调用者可直接对账当前状态无需观察事件边沿服务定位与生命周期WhenReady 阶段的单例PowerService的类声明清晰交代了它在应用生命周期中的位置Injectable(PowerService) ServicePhase(Phase.WhenReady) export class PowerService extends BaseService {通过Injectable(PowerService)注册进依赖容器其他模块用application.get(PowerService)获取通过ServicePhase(Phase.WhenReady)声明在应用“已就绪”阶段初始化。此时app.whenReady()已经完成powerSaveBlocker与BrowserWindow可以直接使用无需再做 whenReady 体操继承 BaseService位于 src/main/core/lifecycle/BaseService.ts获得统一的onInit()/onStop()钩子与registerDisposable()资源注册能力——凡是registerDisposable()注册的监听器、事件订阅都会在服务停止时自动清理。初始化时onInit()依次执行三件事PowerService.tsprotected onInit(): void { this.initPowerEvents() // 电源通知事件 去重状态机 this.initShutdownBarrier() // 跨平台关机屏障 this.initSleepPrevention() // 防睡眠 偏好门控 logger.info(PowerService initialized, { platform: process.platform }) }onStop()则会清空关机处理器列表、停止正在运行的防睡眠阻塞器powerSaveBlocker.stop并清空 holds 表PowerService.ts保证服务停止后不留任何系统级副作用。电源通知事件类型化 Emitter 与去重状态机事件面Event Surface服务对外暴露五个事件全部是“私有Emitter 公有只读Event”的经典封装模式public readonly onSuspend: Eventvoid public readonly onResume: Eventvoid public readonly onLockScreen: Eventvoid public readonly onUnlockScreen: Eventvoid public readonly onPowerSourceChange: EventPowerSource调用方只拿到只读的Event订阅/退订能力无法直接 fire保证事件只能由服务内部产生。去重逻辑两种不同的策略初始化时服务先用当前状态播种电源来源保证第一次查询/订阅即正确this.powerSource powerMonitor.onBatteryPower ? battery : ac随后注册六个底层监听器。其中suspend/resume 走状态机去重因为 macOS 存在双触发问题Electron 上游 issue #24803const onSuspend () { if (this.powerPhase suspended) return // 已挂起忽略重复触发 this.powerPhase suspended this._onSuspend.fire() } const onResume () { if (this.powerPhase active) return this.powerPhase active this._onResume.fire() }核心思路是维护powerPhase: active | suspended内部状态只有发生真实的“active → suspended”或“suspended → active”状态迁移时才对外触发一次事件重复事件被内部状态直接吞掉。lock/unlock 则直接透传——锁屏/解锁没有可去重的状态机也不需要const onLockScreen () this._onLockScreen.fire() const onUnlockScreen () this._onUnlockScreen.fire()电源来源AC/电池同样按“变化才触发”去重updatePowerSource(source)只有在来源真正变化时才更新内部状态并 fireonPowerSourceChange。所有底层powerMonitor监听器都通过registerDisposable注册服务停止时统一removeListener。测试 PowerService.test.ts 对上述行为做了精确断言连续两次suspend事件只 fire 一次onSuspendresume后又suspend会再次触发未挂起时收到resume不会触发onResumelock-screen触发两次则onLockScreen收到两次透传语义初始化时onBatteryPowerfalse则getPowerSource()返回ac重复on-battery不重复 fire。关机屏障串行执行、错误隔离、硬超时系统关机时应用往往还有未落盘的状态消息、任务元数据、缓存。PowerService提供registerShutdownHandler(fn)让各模块登记清理逻辑返回的Disposable可随时注销服务停止时也会清空全部处理器public registerShutdownHandler(handler: ShutdownHandler): Disposable { this.shutdownHandlers.push(handler) return { dispose: () { const idx this.shutdownHandlers.indexOf(handler) if (idx ! -1) this.shutdownHandlers.splice(idx, 1) } } }执行语义串行 错误隔离 5 秒硬超时executeShutdownHandlers()是屏障的核心PowerService.ts串行执行for 循环依次await每个 handler错误隔离单个 handler 抛错只记录日志不影响后续 handler 执行硬超时兜底Promise.race([run, timeout])其中超时上限由常量SHUTDOWN_HANDLER_TIMEOUT_MS 50005 秒控制。一旦超时记录警告并直接放行退出一个卡死的 handler 永远无法阻止用户关机。跨平台接入macOS/Linux 与 Windows 两条路径initShutdownBarrier()按平台分流macOS/Linux ——powerMonitor的shutdown事件 preventDefault()const shutdownListener async (event?: Electron.Event) { event?.preventDefault() try { await this.executeShutdownHandlers() } finally { application.quit() } } powerMonitor.on(shutdown, shutdownListener)注意一个实现细节Electron 的类型定义中shutdown监听器签名是() void省略了事件参数但运行时确实会传入带preventDefault的事件对象。源码将事件参数声明为可选既保持与类型重载的兼容又能在运行时调用preventDefault()推迟关机为处理器争取执行窗口。Windows ——paymoapp/electron-shutdown-handler原生插件Windows 路径更复杂关键点在于插件钩住 Windows 关机消息WM_QUERYENDSESSION需要一个原生窗口句柄HWND。服务刻意创建自己的隐藏窗口而非复用主窗口——主窗口是单例可能被销毁重建HWND 会变化、初始化时可能还不存在复用会把手伸进主窗口生命周期造成耦合隐藏窗口采用show: falsepaintWhenInitiallyHidden: falseskipTaskbar: true的极简配置。由于不加载任何内容且禁止渲染器激活/绘制Electron 不会为该窗口派生独立的渲染进程——这才是控制内存的关键杠杆窗口尺寸并不影响内存width/height: 0也只会被钳制到平台最小值源码注释给出了实测边际成本在窗口子系统已被主窗口初始化的前提下约 0.7 MB RSS、零新增进程必须显式调用blockShutdown(...)才能真正阻止 Windows 关机否则插件只“观察”事件而不持有系统——这是该路径能成为真正屏障的关键必须在监听器挂上之后再调用收到关机信号后的流程为blockShutdown→ 执行处理器 →releaseShutdown()释放阻塞 →application.quit()。ElectronShutdownHandler.setWindowHandle(shutdownHookWindow.getNativeWindowHandle()) ElectronShutdownHandler.on(shutdown, async () { try { await this.executeShutdownHandlers() } finally { ElectronShutdownHandler.releaseShutdown() application.quit() } }) ElectronShutdownHandler.blockShutdown(Cherry Studio is finishing background work)关机走应用正常退出流程的意义无论哪条平台路径最终都调用application.quit()而非裸app.quit()。这会维持应用内部的_isQuitting记账状态因此操作系统发起的关机与用户主动退出走同一条before-quit流程——若存在活跃的Application.preventQuithold例如正在进行数据迁移同样会闸门 OS 关机最终由 5 秒硬超时兜底因为 OS 不可能被无限期阻塞。测试覆盖了关键场景PowerService.test.tsshutdown 事件触发 preventDefault 并执行 handler 后 quit某个 handler 抛错不影响其他 handler 与 quithandler 永不 resolve 时 5 秒假时钟推进后强制 quitDisposable注销后 handler 不再执行Windows 路径验证setWindowHandle、blockShutdown、releaseShutdown与 quit 的完整调用链。防睡眠机制引用计数 holds 偏好门控preventSleep()是“让机器保持唤醒”的统一入口语义与Application.preventQuit(reason)的 hold 惯用法对称——这是一个请求而非硬保证public preventSleep(reason?: string): Disposable { const token Symbol(reason ?? sleep-prevention) this.holds.set(token, { reason, since: Date.now() }) this.applyBlockerState() return { dispose: () { if (this.holds.delete(token)) this.applyBlockerState() } } }为什么用 Map 而不是计数器holds 是一张Mapsymbol, { reason?: string; since: number }而非简单的数字计数。好处有二dispose 幂等Map.delete对重复调用返回false第二次 dispose 不会再次触发状态收敛天然幂等可枚举诊断每个 hold 记录reason与since持有时间戳一旦出现“机器始终不睡”的疑似泄漏可以枚举出“谁在持有、从何时开始”。单一幂等收敛点applyBlockerStateOS 阻塞器的启停全部收敛到applyBlockerState()这一个幂等函数private applyBlockerState(): void { const shouldBlock this.preventEnabled this.holds.size 0 try { if (shouldBlock this.blockerId null) { this.blockerId powerSaveBlocker.start(prevent-app-suspension) } else if (!shouldBlock this.blockerId ! null) { powerSaveBlocker.stop(this.blockerId) this.blockerId null } } catch (err) { logger.warn(powerSaveBlocker state change failed; ...) } }激活条件 偏好开启 且 至少一个 hold与门逻辑二者缺一不可阻塞类型选prevent-app-suspension保持系统运行但允许显示器休眠——正好契合后台任务任务/下载场景prevent-display-sleep则会让显示器也常亮不适用永不抛出任何powerSaveBlocker失败都被记录并吞掉。这意味着preventSleep()永远返回可用的Disposable调用方无需任何防御性 try/catch——优雅降级收敛在服务内部而不是散落在每个调用点。失败后果是暂时性的下一次 preventSleep/dispose/偏好变更会重新执行收敛并可能恢复isPreventingSleep()返回“偏好开启 且 holds 非空”的合成状态即当前是否真正在阻止睡眠。测试对此有完整覆盖偏好关闭时即使有 hold 也不 start偏好开启后首个 hold 触发 start参数确认为prevent-app-suspension多个 hold 只维护一个 blocker最后一个释放才 stopdispose 幂等powerSaveBlocker.start抛错时 preventSleep 不抛且仍返回可用 hold偏好中途关闭立即 stop blocker偏好中途开启且已有 hold 则立即 start。偏好门控配置定义与设置项偏好键app.power.prevent_sleep_when_busy定义在 scripts/data-classify/data/target-key-definitions.json 中{ targetKey: app.power.prevent_sleep_when_busy, type: boolean, defaultValue: false, status: classified, description: Prevent system sleep while the app has active work (v2 new feature, no v1 source) }要点类型boolean默认false即默认不干预系统睡眠把控制权交给用户。服务通过PreferenceService自读该键并在变更时订阅响应subscribeChange从而在“用户正在设置里切换开关”时也能即时启停 blocker。服务初始化时读取的偏好门控逻辑与TrayService/ThemeService/ProxyService的自读模式一致。对应的用户可见开关位于设置 → 通用Settings → General实现见 GeneralSettings.tsxsetting-general-prevent-sleep-when-busy设置行通过usePreference(app.power.prevent_sleep_when_busy)与偏好存储双向绑定。查询 API电平触发迟到订阅者也能对账服务提供五个查询方法全部直接透传powerMonitor或返回内部状态getPowerPhase(): PowerPhase // active | suspended内部状态机 getPowerSource(): PowerSource // ac | battery | unknown内部状态 isOnBatteryPower(): boolean // 透传 powerMonitor.onBatteryPower getSystemIdleTime(): number // 透传 powerMonitor.getSystemIdleTime()单位秒 getSystemIdleState(idleThresholdSec: number): SystemIdleState // active | idle | locked | unknown设计上它们是电平触发level-triggered而非边沿触发查询返回的是“当前状态”而不是“刚刚发生了什么”。一个迟到的订阅者无需亲历事件边沿直接查询即可与当前系统状态对账。测试验证了 idle 查询参数正确透传getSystemIdleState(60)→idle与isOnBatteryPower转发。源码注释还透露了后续用途空闲查询将为 Job 系统的after-idle追赶策略解锁能力。快速上手三段式接入模板以下完整代码直接取自原文档 Quick Start是接入PowerService的标准姿势import { application } from application const power application.get(PowerService) // 1. 为某段工作期间保持机器唤醒仅当用户开启了 // app.power.prevent_sleep_when_busy 时才会真正生效 const hold power.preventSleep(job:export) try { await doWork() } finally { hold.dispose() // 幂等 } // 2. 响应机器挂起/恢复例如暂停/恢复长轮询 this.registerDisposable(power.onSuspend(() pauseLongPoll())) this.registerDisposable(power.onResume(() resumeLongPoll())) // 3. 在 OS 关机前执行清理 this.registerDisposable(power.registerShutdownHandler(() flushCriticalState()))三条使用建议always 配对释放preventSleep的 hold 必须与工作代码块生命周期一致惯用法是try/finally包裹finally中dispose()事件订阅用 registerDisposableonSuspend/onResume等订阅返回的Disposable应交给BaseService.registerDisposable随宿主服务停止自动退订无需防御性守卫preventSleep永不抛出、永远返回可用 Disposable调用方不需要 try/catch 包裹获取过程。第一个注册者Job 系统如何持有防睡眠 hold防睡眠是一个通用注册表任何需要机器保持唤醒的 worker 都来注册一个 hold而“是否允许”的闸门用户偏好正交地由本服务持有。Job 系统是第一个注册者。在 JobManager.ts 的任务执行体中可以看到实际调用const sleepHold application.get(PowerService).preventSleep(job:${row.type}:${row.id}) try { const output await handler.execute(ctx) ... } catch (err) { ... }实现细节值得留意hold 的 reason 携带任务类型与 IDjob:${row.type}:${row.id}便于按任务诊断该 hold 声明在任务 IIFE 作用域内finally分支统一 dispose——每次尝试attempt独立持有重试之间任务处于delayed非工作中状态不应持续阻止睡眠注释明确preventSleep永不抛出、总是返回 Disposable提供方内部降级因此这里无需守卫代码。这一注册时机也印证了原文档 Notes 中的规划流式streaming与其他 worker 后续将通过同一 API 自注册。工程要点与设计启示原文档 Notes 部分集中了本服务最值得借鉴的工程决策逐一展开WhenReady 阶段直接使用系统 API。应用已就绪powerSaveBlocker/BrowserWindow直接可用不需要app.whenReady()体操偏好门控采用自读模式与TrayService/ThemeService/ProxyService一致。防睡眠是通用注册表。任何 worker 都可注册 hold用户偏好这道闸门与本服务正交且归属本服务所有。Job 系统是首个注册者流式与其他 worker 后续自注册。preventSleep()尽力而为、永不抛出。永远返回可用DisposablepowerSaveBlocker的任何失败都被内部记录并吞掉。调用方因此无需在获取处做防御性 try/catch——优雅降级住在提供方而不是散落在每个调用点。OS 关机走应用正常退出流程。macOS/Linux 为event.preventDefault()→application.quit()Windows 为blockShutdown→ handlers →releaseShutdown→application.quit()。因为退出经过before-quit活跃的Application.preventQuithold如数据迁移会像拦截用户退出一样拦截 OS 关机——并以硬超时兜底OS 不能被无限期阻塞。用户开关位置设置 → 通用偏好定义在 contenteditable="false">【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考