fix: add PowerMonitorService for system shutdown handling (#11115)

* feat: add PowerMonitorService for system shutdown handling

- Add PowerMonitorService to monitor system shutdown events
- Use @paymoapp/electron-shutdown-handler for Windows platform
- Use Electron's powerMonitor for macOS and Linux platforms
- Support registering multiple shutdown handlers via dependency injection
- Register shutdown handlers in ipc.ts to disable auto-update and save data

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* format code

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
beyondkmp 2025-11-04 18:56:09 +08:00 committed by GitHub
parent 7dce1d776b
commit 5fea202a7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 196 additions and 0 deletions

View File

@ -82,6 +82,7 @@
"@libsql/client": "0.14.0",
"@libsql/win32-x64-msvc": "^0.4.7",
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
"@paymoapp/electron-shutdown-handler": "^1.1.2",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"express": "^5.1.0",
"font-list": "^2.0.0",

View File

@ -21,6 +21,7 @@ import { appMenuService } from './services/AppMenuService'
import { configManager } from './services/ConfigManager'
import mcpService from './services/MCPService'
import { nodeTraceService } from './services/NodeTraceService'
import powerMonitorService from './services/PowerMonitorService'
import {
CHERRY_STUDIO_PROTOCOL,
handleProtocolUrl,
@ -132,6 +133,7 @@ if (!app.requestSingleInstanceLock()) {
appMenuService?.setupApplicationMenu()
nodeTraceService.init()
powerMonitorService.init()
app.on('activate', function () {
const mainWindow = windowService.getMainWindow()

View File

@ -50,6 +50,7 @@ import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService'
import { ocrService } from './services/ocr/OcrService'
import OvmsManager from './services/OvmsManager'
import powerMonitorService from './services/PowerMonitorService'
import { proxyManager } from './services/ProxyManager'
import { pythonService } from './services/PythonService'
import { FileServiceManager } from './services/remotefile/FileServiceManager'
@ -115,6 +116,18 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const appUpdater = new AppUpdater()
const notificationService = new NotificationService()
// Register shutdown handlers
powerMonitorService.registerShutdownHandler(() => {
appUpdater.setAutoUpdate(false)
})
powerMonitorService.registerShutdownHandler(() => {
const mw = windowService.getMainWindow()
if (mw && !mw.isDestroyed()) {
mw.webContents.send(IpcChannel.App_SaveData)
}
})
const checkMainWindow = () => {
if (!mainWindow || mainWindow.isDestroyed()) {
throw new Error('Main window does not exist or has been destroyed')

View File

@ -0,0 +1,112 @@
import { loggerService } from '@logger'
import { isLinux, isMac, isWin } from '@main/constant'
import ElectronShutdownHandler from '@paymoapp/electron-shutdown-handler'
import { BrowserWindow } from 'electron'
import { powerMonitor } from 'electron'
const logger = loggerService.withContext('PowerMonitorService')
type ShutdownHandler = () => void | Promise<void>
export class PowerMonitorService {
private static instance: PowerMonitorService
private initialized = false
private shutdownHandlers: ShutdownHandler[] = []
private constructor() {
// Private constructor to prevent direct instantiation
}
public static getInstance(): PowerMonitorService {
if (!PowerMonitorService.instance) {
PowerMonitorService.instance = new PowerMonitorService()
}
return PowerMonitorService.instance
}
/**
* Register a shutdown handler to be called when system shutdown is detected
* @param handler - The handler function to be called on shutdown
*/
public registerShutdownHandler(handler: ShutdownHandler): void {
this.shutdownHandlers.push(handler)
logger.info('Shutdown handler registered', { totalHandlers: this.shutdownHandlers.length })
}
/**
* Initialize power monitor to listen for shutdown events
*/
public init(): void {
if (this.initialized) {
logger.warn('PowerMonitorService already initialized')
return
}
if (isWin) {
this.initWindowsShutdownHandler()
} else if (isMac || isLinux) {
this.initElectronPowerMonitor()
}
this.initialized = true
logger.info('PowerMonitorService initialized', { platform: process.platform })
}
/**
* Execute all registered shutdown handlers
*/
private async executeShutdownHandlers(): Promise<void> {
logger.info('Executing shutdown handlers', { count: this.shutdownHandlers.length })
for (const handler of this.shutdownHandlers) {
try {
await handler()
} catch (error) {
logger.error('Error executing shutdown handler', error as Error)
}
}
}
/**
* Initialize shutdown handler for Windows using @paymoapp/electron-shutdown-handler
*/
private initWindowsShutdownHandler(): void {
try {
const zeroMemoryWindow = new BrowserWindow({ show: false })
// Set the window handle for the shutdown handler
ElectronShutdownHandler.setWindowHandle(zeroMemoryWindow.getNativeWindowHandle())
// Listen for shutdown event
ElectronShutdownHandler.on('shutdown', async () => {
logger.info('System shutdown event detected (Windows)')
// Execute all registered shutdown handlers
await this.executeShutdownHandlers()
// Release the shutdown block to allow the system to shut down
ElectronShutdownHandler.releaseShutdown()
})
logger.info('Windows shutdown handler registered')
} catch (error) {
logger.error('Failed to initialize Windows shutdown handler', error as Error)
}
}
/**
* Initialize power monitor for macOS and Linux using Electron's powerMonitor
*/
private initElectronPowerMonitor(): void {
try {
powerMonitor.on('shutdown', async () => {
logger.info('System shutdown event detected', { platform: process.platform })
// Execute all registered shutdown handlers
await this.executeShutdownHandlers()
})
logger.info('Electron powerMonitor shutdown listener registered')
} catch (error) {
logger.error('Failed to initialize Electron powerMonitor', error as Error)
}
}
}
// Default export as singleton instance
export default PowerMonitorService.getInstance()

View File

@ -6787,6 +6787,17 @@ __metadata:
languageName: node
linkType: hard
"@paymoapp/electron-shutdown-handler@npm:^1.1.2":
version: 1.1.2
resolution: "@paymoapp/electron-shutdown-handler@npm:1.1.2"
dependencies:
node-addon-api: "npm:^5.0.0"
node-gyp: "npm:latest"
prebuild-install: "npm:^7.1.2"
checksum: 10c0/c774ded900870cd0eae79f2281e971561328b9d2f555b8763a75773f12d953edfa3923067f257bbda5001f9c934343d55344f7a7aac5eff8739762c91e9f37a7
languageName: node
linkType: hard
"@pdf-lib/standard-fonts@npm:^1.0.0":
version: 1.0.0
resolution: "@pdf-lib/standard-fonts@npm:1.0.0"
@ -12816,6 +12827,7 @@ __metadata:
"@opentelemetry/sdk-trace-node": "npm:^2.0.0"
"@opentelemetry/sdk-trace-web": "npm:^2.0.0"
"@opeoginni/github-copilot-openai-compatible": "npm:0.1.19"
"@paymoapp/electron-shutdown-handler": "npm:^1.1.2"
"@playwright/test": "npm:^1.52.0"
"@radix-ui/react-context-menu": "npm:^2.2.16"
"@reduxjs/toolkit": "npm:^2.2.5"
@ -16036,6 +16048,13 @@ __metadata:
languageName: node
linkType: hard
"detect-libc@npm:^2.0.0":
version: 2.1.2
resolution: "detect-libc@npm:2.1.2"
checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4
languageName: node
linkType: hard
"detect-libc@npm:^2.0.1":
version: 2.0.3
resolution: "detect-libc@npm:2.0.3"
@ -22425,6 +22444,13 @@ __metadata:
languageName: node
linkType: hard
"napi-build-utils@npm:^2.0.0":
version: 2.0.0
resolution: "napi-build-utils@npm:2.0.0"
checksum: 10c0/5833aaeb5cc5c173da47a102efa4680a95842c13e0d9cc70428bd3ee8d96bb2172f8860d2811799b5daa5cbeda779933601492a2028a6a5351c6d0fcf6de83db
languageName: node
linkType: hard
"native-promise-only@npm:0.8.1":
version: 0.8.1
resolution: "native-promise-only@npm:0.8.1"
@ -22508,6 +22534,15 @@ __metadata:
languageName: node
linkType: hard
"node-addon-api@npm:^5.0.0":
version: 5.1.0
resolution: "node-addon-api@npm:5.1.0"
dependencies:
node-gyp: "npm:latest"
checksum: 10c0/0eb269786124ba6fad9df8007a149e03c199b3e5a3038125dfb3e747c2d5113d406a4e33f4de1ea600aa2339be1f137d55eba1a73ee34e5fff06c52a5c296d1d
languageName: node
linkType: hard
"node-addon-api@npm:^8.4.0":
version: 8.4.0
resolution: "node-addon-api@npm:8.4.0"
@ -23722,6 +23757,28 @@ __metadata:
languageName: node
linkType: hard
"prebuild-install@npm:^7.1.2":
version: 7.1.3
resolution: "prebuild-install@npm:7.1.3"
dependencies:
detect-libc: "npm:^2.0.0"
expand-template: "npm:^2.0.3"
github-from-package: "npm:0.0.0"
minimist: "npm:^1.2.3"
mkdirp-classic: "npm:^0.5.3"
napi-build-utils: "npm:^2.0.0"
node-abi: "npm:^3.3.0"
pump: "npm:^3.0.0"
rc: "npm:^1.2.7"
simple-get: "npm:^4.0.0"
tar-fs: "npm:^2.0.0"
tunnel-agent: "npm:^0.6.0"
bin:
prebuild-install: bin.js
checksum: 10c0/25919a42b52734606a4036ab492d37cfe8b601273d8dfb1fa3c84e141a0a475e7bad3ab848c741d2f810cef892fcf6059b8c7fe5b29f98d30e0c29ad009bedff
languageName: node
linkType: hard
"prelude-ls@npm:^1.2.1":
version: 1.2.1
resolution: "prelude-ls@npm:1.2.1"
@ -26301,6 +26358,17 @@ __metadata:
languageName: node
linkType: hard
"simple-get@npm:^4.0.0":
version: 4.0.1
resolution: "simple-get@npm:4.0.1"
dependencies:
decompress-response: "npm:^6.0.0"
once: "npm:^1.3.1"
simple-concat: "npm:^1.0.0"
checksum: 10c0/b0649a581dbca741babb960423248899203165769747142033479a7dc5e77d7b0fced0253c731cd57cf21e31e4d77c9157c3069f4448d558ebc96cf9e1eebcf0
languageName: node
linkType: hard
"simple-swizzle@npm:^0.2.2":
version: 0.2.2
resolution: "simple-swizzle@npm:0.2.2"