diff --git a/package.json b/package.json index f0c0050d2..c7a3a66ba 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main/index.ts b/src/main/index.ts index bcec606f4..025268b65 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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() diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 936a0dd00..5bf5c7305 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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') diff --git a/src/main/services/PowerMonitorService.ts b/src/main/services/PowerMonitorService.ts new file mode 100644 index 000000000..aab3906c9 --- /dev/null +++ b/src/main/services/PowerMonitorService.ts @@ -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 + +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 { + 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() diff --git a/yarn.lock b/yarn.lock index 73436f510..9415540a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"