feat: add graceful shutdown
[e-mobility-charging-stations-simulator.git] / src / charging-station / Bootstrap.ts
index f53225324ae21083fe0d82f6aa27363cb3523279..4a6bd75c2d7a2ebbce2e6ebb24c3ceab9019dba6 100644 (file)
@@ -2,12 +2,13 @@
 
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
-import { type Worker, isMainThread } from 'worker_threads';
+import { type Worker, isMainThread } from 'node:worker_threads';
 
 import chalk from 'chalk';
 
-import { type AbstractUIServer, ChargingStationUtils, UIServerFactory } from './internal';
-import { version } from '../../package.json';
+import type { AbstractUIServer } from './ui-server/AbstractUIServer';
+import { UIServerFactory } from './ui-server/UIServerFactory';
+import packageJson from '../../package.json' assert { type: 'json' };
 import { BaseError } from '../exception';
 import { type Storage, StorageFactory } from '../performance';
 import {
@@ -16,12 +17,11 @@ import {
   type ChargingStationWorkerMessage,
   type ChargingStationWorkerMessageData,
   ChargingStationWorkerMessageEvents,
+  ProcedureName,
   type StationTemplateUrl,
   type Statistics,
 } from '../types';
-import { Configuration } from '../utils/Configuration';
-import { logger } from '../utils/Logger';
-import { Utils } from '../utils/Utils';
+import { Configuration, Constants, ErrorUtils, Utils, logger } from '../utils';
 import { type MessageHandler, type WorkerAbstract, WorkerFactory } from '../worker';
 
 const moduleName = 'Bootstrap';
@@ -39,22 +39,26 @@ export class Bootstrap {
   private readonly uiServer!: AbstractUIServer | null;
   private readonly storage!: Storage;
   private numberOfStartedChargingStations!: number;
-  private readonly version: string = version;
+  private readonly version: string = packageJson.version;
   private initializedCounters: boolean;
   private started: boolean;
   private readonly workerScript: string;
 
   private constructor() {
+    for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
+      process.on(signal, () => {
+        this.gracefulShutdown().catch(Constants.EMPTY_FUNCTION);
+      });
+    }
     // Enable unconditionally for now
-    this.logUnhandledRejection();
-    this.logUncaughtException();
+    ErrorUtils.handleUnhandledRejection();
+    ErrorUtils.handleUncaughtException();
     this.initializedCounters = false;
     this.started = false;
     this.initializeCounters();
     this.workerImplementation = null;
     this.workerScript = path.join(
-      path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
-      'charging-station',
+      path.dirname(fileURLToPath(import.meta.url)),
       `ChargingStationWorker${path.extname(fileURLToPath(import.meta.url))}`
     );
     Configuration.getUIServer().enabled === true &&
@@ -103,11 +107,11 @@ export class Bootstrap {
           `Charging stations simulator ${
             this.version
           } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
-            ChargingStationUtils.workerDynamicPoolInUse()
+            Configuration.workerDynamicPoolInUse()
               ? `${Configuration.getWorker().poolMinSize?.toString()}/`
               : ''
           }${this.workerImplementation?.size}${
-            ChargingStationUtils.workerPoolInUse()
+            Configuration.workerPoolInUse()
               ? `/${Configuration.getWorker().poolMaxSize?.toString()}`
               : ''
           } worker(s) concurrently running in '${Configuration.getWorker().processType}' mode${
@@ -125,6 +129,11 @@ export class Bootstrap {
 
   public async stop(): Promise<void> {
     if (isMainThread && this.started === true) {
+      await this.uiServer?.sendBroadcastChannelRequest(
+        Utils.generateUUID(),
+        ProcedureName.STOP_CHARGING_STATION,
+        Constants.EMPTY_FREEZED_OBJECT
+      );
       await this.workerImplementation?.stop();
       this.workerImplementation = null;
       this.uiServer?.stop();
@@ -172,16 +181,16 @@ export class Bootstrap {
     // );
     try {
       switch (msg.id) {
-        case ChargingStationWorkerMessageEvents.STARTED:
+        case ChargingStationWorkerMessageEvents.started:
           this.workerEventStarted(msg.data as ChargingStationData);
           break;
-        case ChargingStationWorkerMessageEvents.STOPPED:
+        case ChargingStationWorkerMessageEvents.stopped:
           this.workerEventStopped(msg.data as ChargingStationData);
           break;
-        case ChargingStationWorkerMessageEvents.UPDATED:
+        case ChargingStationWorkerMessageEvents.updated:
           this.workerEventUpdated(msg.data as ChargingStationData);
           break;
-        case ChargingStationWorkerMessageEvents.PERFORMANCE_STATISTICS:
+        case ChargingStationWorkerMessageEvents.performanceStatistics:
           this.workerEventPerformanceStatistics(msg.data as Statistics);
           break;
         default:
@@ -238,9 +247,9 @@ export class Bootstrap {
       const stationTemplateUrls = Configuration.getStationTemplateUrls();
       if (Utils.isNotEmptyArray(stationTemplateUrls)) {
         this.numberOfChargingStationTemplates = stationTemplateUrls.length;
-        stationTemplateUrls.forEach((stationTemplateUrl) => {
+        for (const stationTemplateUrl of stationTemplateUrls) {
           this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0;
-        });
+        }
       } else {
         console.warn(
           chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
@@ -258,18 +267,6 @@ export class Bootstrap {
     }
   }
 
-  private logUncaughtException(): void {
-    process.on('uncaughtException', (error: Error) => {
-      console.error(chalk.red('Uncaught exception: '), error);
-    });
-  }
-
-  private logUnhandledRejection(): void {
-    process.on('unhandledRejection', (reason: unknown) => {
-      console.error(chalk.red('Unhandled rejection: '), reason);
-    });
-  }
-
   private async startChargingStation(
     index: number,
     stationTemplateUrl: StationTemplateUrl
@@ -277,7 +274,7 @@ export class Bootstrap {
     await this.workerImplementation?.addElement({
       index,
       templateFile: path.join(
-        path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
+        path.dirname(fileURLToPath(import.meta.url)),
         'assets',
         'station-templates',
         stationTemplateUrl.file
@@ -285,6 +282,16 @@ export class Bootstrap {
     });
   }
 
+  private gracefulShutdown = async (): Promise<void> => {
+    console.info(`${chalk.green('Graceful shutdown')}`);
+    try {
+      await this.stop();
+      process.exit(0);
+    } catch (error) {
+      process.exit(1);
+    }
+  };
+
   private logPrefix = (): string => {
     return Utils.logPrefix(' Bootstrap |');
   };