From bb05f2cb75dcdbb8fdbc22dfa74773f71c422597 Mon Sep 17 00:00:00 2001 From: HRS Date: Thu, 17 Jul 2025 18:26:12 +0200 Subject: [PATCH] Implement proper shutdown --- src/controller/settings/restartController.ts | 13 +++++++ src/index.ts | 31 +++++++++++++++ src/serviceMap.ts | 8 +++- .../controllerServiceProvider.ts | 3 ++ src/settings/shutdownEventType.ts | 5 +++ src/settings/shutdownManager.ts | 39 +++++++++++++++++++ 6 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/controller/settings/restartController.ts create mode 100644 src/settings/shutdownEventType.ts create mode 100644 src/settings/shutdownManager.ts diff --git a/src/controller/settings/restartController.ts b/src/controller/settings/restartController.ts new file mode 100644 index 00000000..38e7378f --- /dev/null +++ b/src/controller/settings/restartController.ts @@ -0,0 +1,13 @@ +import { Request, Response } from 'express'; +import ControllerInterface from "../controllerInterface.js"; + +export default class RestartController implements ControllerInterface +{ + + public execute(req: Request, res: Response): void + { + res.sendStatus(202); + + process.exit(0); + } +} diff --git a/src/index.ts b/src/index.ts index 0e093a5e..a2aeb6fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,6 +51,7 @@ const logger = container.get('logger.default'); const io = container.get('server.websocket'); const deviceManager = container.get('device.manager'); const settingsManager = container.get('settings.manager'); +const shutdownManager = container.get('shutdown.manager'); const scriptRuntime = container.get('automation.scriptRuntime'); container.get('device.provider.loader').loadFromSettings(); @@ -134,6 +135,10 @@ app.put('/settings', (req, res) => { return controller.execute(req, res) }); +app.get('/restart', (req, res) => { + return container.get('controller.settings.restart').execute(req, res); +}); + // Whenever someone connects this gets executed io.on('connection', socket => { logger.debug(`Client connected: ${socket.id}`); @@ -178,6 +183,32 @@ httpServer.listen(APP_PORT, () => { logger.info(`SlvCtrl+ server listening on port ${APP_PORT}!`); }); +const gracefulShutdown = async () => { + console.log('Starting graceful shutdown...'); + await shutdownManager.runBeforeShutdown(); + + httpServer.close(() => { + console.log('HTTP server closed'); + process.exit(0); + }); + + setTimeout(() => { + console.error('Forced shutdown after timeout'); + process.exit(1); + }, 5000); +} + +const handleSignal = (signal: string) => { + console.log(`Received ${signal}`); + gracefulShutdown().catch((err) => { + console.error('Error during graceful shutdown:', err); + process.exit(1); + }); +}; + +process.on('SIGINT', handleSignal); +process.on('SIGTERM', handleSignal); + process.on('uncaughtException', (err: Error) => { logger.error('Asynchronous error caught.', err); }); diff --git a/src/serviceMap.ts b/src/serviceMap.ts index d670a2d9..8c95a939 100644 --- a/src/serviceMap.ts +++ b/src/serviceMap.ts @@ -30,6 +30,9 @@ import GetLogController from "./controller/automation/getLogController.js"; import RunScriptController from "./controller/automation/runScriptController.js"; import StopScriptController from "./controller/automation/stopScriptController.js"; import StatusScriptController from "./controller/automation/statusScriptController.js"; +import GetSettingsController from "./controller/settings/getSettingsController.js"; +import PutSettingsController from "./controller/settings/putSettingsController.js"; +import RestartController from "./controller/settings/restartController.js"; import DelegatedVirtualDeviceFactory from "./device/protocol/virtual/delegatedVirtualDeviceFactory.js"; import VirtualDeviceProvider from "./device/protocol/virtual/virtualDeviceProvider.js"; import VirtualDeviceProviderFactory from "./device/protocol/virtual/virtualDeviceProviderFactory.js"; @@ -37,10 +40,9 @@ import GenericVirtualDeviceFactory from "./device/protocol/virtual/genericVirtua import DisplayVirtualDeviceLogic from "./device/protocol/virtual/display/displayVirtualDeviceLogic.js"; import RandomGeneratorVirtualDeviceLogic from "./device/protocol/virtual/randomGenerator/randomGeneratorVirtualDeviceLogic.js"; import TtsVirtualDeviceLogic from "./device/protocol/virtual/audio/ttsVirtualDeviceLogic.js"; -import GetSettingsController from "./controller/settings/getSettingsController.js"; -import PutSettingsController from "./controller/settings/putSettingsController.js"; import JsonSchemaValidatorFactory from "./schemaValidation/JsonSchemaValidatorFactory.js"; import JsonSchemaValidator from "./schemaValidation/JsonSchemaValidator.js"; +import ShutdownManager from "./settings/shutdownManager.js"; import Ajv from "ajv/dist/2020.js"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -83,6 +85,7 @@ type ServiceMap = { 'settings': Settings, 'settings.manager': SettingsManager, 'settings.schema.validator': JsonSchemaValidator, + 'shutdown.manager': ShutdownManager, /* automationServiceProvider */ 'automation.scriptRuntime': ScriptRuntime, @@ -109,6 +112,7 @@ type ServiceMap = { 'controller.automation.statusScript': StatusScriptController, 'controller.settings.get': GetSettingsController, 'controller.settings.put': PutSettingsController, + 'controller.settings.restart': RestartController, } export default ServiceMap; diff --git a/src/serviceProvider/controllerServiceProvider.ts b/src/serviceProvider/controllerServiceProvider.ts index 0208c50f..77292231 100644 --- a/src/serviceProvider/controllerServiceProvider.ts +++ b/src/serviceProvider/controllerServiceProvider.ts @@ -13,6 +13,7 @@ import GetLogController from "../controller/automation/getLogController.js"; import StatusScriptController from "../controller/automation/statusScriptController.js"; import GetSettingsController from "../controller/settings/getSettingsController.js"; import PutSettingsController from "../controller/settings/putSettingsController.js"; +import RestartController from "../controller/settings/restartController.js"; import ServiceMap from "../serviceMap.js"; export default class ControllerServiceProvider implements ServiceProvider @@ -108,5 +109,7 @@ export default class ControllerServiceProvider implements ServiceProvider new RestartController()); } } diff --git a/src/settings/shutdownEventType.ts b/src/settings/shutdownEventType.ts new file mode 100644 index 00000000..41d15c39 --- /dev/null +++ b/src/settings/shutdownEventType.ts @@ -0,0 +1,5 @@ +enum ShutdownEventType { + beforeShutdown = "beforeShutdown", +} + +export default ShutdownEventType; diff --git a/src/settings/shutdownManager.ts b/src/settings/shutdownManager.ts new file mode 100644 index 00000000..f864e816 --- /dev/null +++ b/src/settings/shutdownManager.ts @@ -0,0 +1,39 @@ +import EventEmitter from "events"; +import Logger from "../logging/Logger"; +import ShutdownEventType from "./shutdownEventType.js"; + +type ShutdownHook = () => Promise; + +type ShutdownEvents = { + [ShutdownEventType.beforeShutdown]: ShutdownHook, +} + +export default class ShutdownManager { + + private readonly eventEmitter: EventEmitter; + + private readonly logger: Logger; + + public constructor(eventEmitter: EventEmitter) { + this.eventEmitter = eventEmitter; + } + + public async runBeforeShutdown(): Promise { + const listeners = this.eventEmitter.listeners(ShutdownEventType.beforeShutdown) as ShutdownHook[]; + this.logger.info(`Running ${listeners.length} shutdown hook(s)...`); + + const results = await Promise.allSettled(listeners.map(fn => fn())); + + for (const [index, result] of results.entries()) { + if (result.status === 'rejected') { + this.logger.error(`Shutdown hook #${index + 1} failed:`, result.reason); + } + } + } + + public on (event: E, listener: ShutdownEvents[E]): this + { + this.eventEmitter.on(event, listener as (...args: any[]) => void); + return this; + } +}