fix: only send UI server response when needed
[e-mobility-charging-stations-simulator.git] / src / charging-station / Bootstrap.ts
index c94a92048fc5a8845feabdb826512b546f5bd848..a1ee4ff2342b01854ffd913ff83ec1dec1abbd0b 100644 (file)
@@ -1,12 +1,12 @@
 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
 
+import { EventEmitter } from 'node:events';
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import { type Worker, isMainThread } from 'node:worker_threads';
 
 import chalk from 'chalk';
 
-import { ChargingStationUtils } from './ChargingStationUtils';
 import type { AbstractUIServer } from './ui-server/AbstractUIServer';
 import { UIServerFactory } from './ui-server/UIServerFactory';
 import packageJson from '../../package.json' assert { type: 'json' };
@@ -18,10 +18,11 @@ import {
   type ChargingStationWorkerMessage,
   type ChargingStationWorkerMessageData,
   ChargingStationWorkerMessageEvents,
+  ProcedureName,
   type StationTemplateUrl,
   type Statistics,
 } from '../types';
-import { Configuration, ErrorUtils, Utils, logger } from '../utils';
+import { Configuration, Constants, ErrorUtils, Utils, logger } from '../utils';
 import { type MessageHandler, type WorkerAbstract, WorkerFactory } from '../worker';
 
 const moduleName = 'Bootstrap';
@@ -31,7 +32,7 @@ enum exitCodes {
   noChargingStationTemplates = 2,
 }
 
-export class Bootstrap {
+export class Bootstrap extends EventEmitter {
   private static instance: Bootstrap | null = null;
   public numberOfChargingStations!: number;
   public numberOfChargingStationTemplates!: number;
@@ -45,6 +46,10 @@ export class Bootstrap {
   private readonly workerScript: string;
 
   private constructor() {
+    super();
+    for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
+      process.on(signal, this.gracefulShutdown);
+    }
     // Enable unconditionally for now
     ErrorUtils.handleUnhandledRejection();
     ErrorUtils.handleUncaughtException();
@@ -102,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${
@@ -124,6 +129,14 @@ export class Bootstrap {
 
   public async stop(): Promise<void> {
     if (isMainThread && this.started === true) {
+      await this.uiServer?.sendBroadcastChannelRequest(
+        this.uiServer.buildProtocolRequest(
+          Utils.generateUUID(),
+          ProcedureName.STOP_CHARGING_STATION,
+          Constants.EMPTY_FREEZED_OBJECT
+        )
+      );
+      await this.waitForChargingStationsStopped();
       await this.workerImplementation?.stop();
       this.workerImplementation = null;
       this.uiServer?.stop();
@@ -173,15 +186,22 @@ export class Bootstrap {
       switch (msg.id) {
         case ChargingStationWorkerMessageEvents.started:
           this.workerEventStarted(msg.data as ChargingStationData);
+          this.emit(ChargingStationWorkerMessageEvents.started, msg.data as ChargingStationData);
           break;
         case ChargingStationWorkerMessageEvents.stopped:
           this.workerEventStopped(msg.data as ChargingStationData);
+          this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data as ChargingStationData);
           break;
         case ChargingStationWorkerMessageEvents.updated:
           this.workerEventUpdated(msg.data as ChargingStationData);
+          this.emit(ChargingStationWorkerMessageEvents.updated, msg.data as ChargingStationData);
           break;
         case ChargingStationWorkerMessageEvents.performanceStatistics:
           this.workerEventPerformanceStatistics(msg.data as Statistics);
+          this.emit(
+            ChargingStationWorkerMessageEvents.performanceStatistics,
+            msg.data as Statistics
+          );
           break;
         default:
           throw new BaseError(
@@ -272,6 +292,35 @@ export class Bootstrap {
     });
   }
 
+  private gracefulShutdown = (): void => {
+    console.info(`${chalk.green('Graceful shutdown')}`);
+    this.stop()
+      .then(() => {
+        process.exit(0);
+      })
+      .catch((error) => {
+        console.error(chalk.red('Error while stopping charging stations simulator: '), error);
+        process.exit(1);
+      });
+  };
+
+  private waitForChargingStationsStopped = async (
+    stoppedEventsToWait = this.numberOfStartedChargingStations
+  ): Promise<number> => {
+    return new Promise((resolve) => {
+      let stoppedEvents = 0;
+      if (stoppedEventsToWait === 0) {
+        resolve(stoppedEvents);
+      }
+      this.on(ChargingStationWorkerMessageEvents.stopped, () => {
+        ++stoppedEvents;
+        if (stoppedEvents === stoppedEventsToWait) {
+          resolve(stoppedEvents);
+        }
+      });
+    });
+  };
+
   private logPrefix = (): string => {
     return Utils.logPrefix(' Bootstrap |');
   };