fix: ensure charging stations are stopped if UI server is disabled at
authorJérôme Benoit <jerome.benoit@sap.com>
Thu, 9 Nov 2023 20:00:15 +0000 (21:00 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Thu, 9 Nov 2023 20:00:15 +0000 (21:00 +0100)
shutdown

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
src/charging-station/Bootstrap.ts
src/charging-station/ui-server/AbstractUIServer.ts
src/utils/ErrorUtils.ts

index 465d5bf21f98b29d9d410914d6d0a0226f560ce9..2c7f56fe4de94c1d7524e99957e68c1b8503c83c 100644 (file)
@@ -2,7 +2,7 @@
 
 import { EventEmitter } from 'node:events';
 import { dirname, extname, join } from 'node:path';
-import { exit } from 'node:process';
+import process, { exit } from 'node:process';
 import { fileURLToPath } from 'node:url';
 
 import chalk from 'chalk';
@@ -56,7 +56,7 @@ export class Bootstrap extends EventEmitter {
   public numberOfChargingStations!: number;
   public numberOfChargingStationTemplates!: number;
   private workerImplementation: WorkerAbstract<ChargingStationWorkerData> | null;
-  private readonly uiServer!: AbstractUIServer | null;
+  private readonly uiServer: AbstractUIServer | null;
   private readonly storage!: Storage;
   private numberOfStartedChargingStations!: number;
   private readonly version: string = version;
@@ -69,7 +69,7 @@ export class Bootstrap extends EventEmitter {
   private constructor() {
     super();
     for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
-      process.on(signal, this.gracefulShutdown);
+      process.on(signal, this.gracefulShutdown.bind(this));
     }
     // Enable unconditionally for now
     handleUnhandledRejection();
@@ -84,11 +84,9 @@ export class Bootstrap extends EventEmitter {
       dirname(fileURLToPath(import.meta.url)),
       `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
     );
-    const uiServerConfiguration = Configuration.getConfigurationSection<UIServerConfiguration>(
-      ConfigurationSection.uiServer,
+    this.uiServer = UIServerFactory.getUIServerImplementation(
+      Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer),
     );
-    uiServerConfiguration.enabled === true &&
-      (this.uiServer = UIServerFactory.getUIServerImplementation(uiServerConfiguration));
     const performanceStorageConfiguration =
       Configuration.getConfigurationSection<StorageConfiguration>(
         ConfigurationSection.performanceStorage,
@@ -120,7 +118,8 @@ export class Bootstrap extends EventEmitter {
         this.initializeWorkerImplementation(workerConfiguration);
         await this.workerImplementation?.start();
         await this.storage?.open();
-        this.uiServer?.start();
+        Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
+          .enabled === true && this.uiServer?.start();
         // Start ChargingStation object instance in worker thread
         for (const stationTemplateUrl of Configuration.getStationTemplateUrls()!) {
           try {
@@ -173,34 +172,19 @@ export class Bootstrap extends EventEmitter {
     }
   }
 
-  public async stop(waitChargingStationsStopped = true): Promise<void> {
+  public async stop(stopChargingStations = true): Promise<void> {
     if (this.started === true) {
       if (this.stopping === false) {
         this.stopping = true;
-        await this.uiServer?.sendInternalRequest(
-          this.uiServer.buildProtocolRequest(
-            generateUUID(),
-            ProcedureName.STOP_CHARGING_STATION,
-            Constants.EMPTY_FROZEN_OBJECT,
-          ),
-        );
-        if (waitChargingStationsStopped === true) {
-          await Promise.race([
-            waitChargingStationEvents(
-              this,
-              ChargingStationWorkerMessageEvents.stopped,
-              this.numberOfChargingStations,
+        if (stopChargingStations === true) {
+          await this.uiServer?.sendInternalRequest(
+            this.uiServer.buildProtocolRequest(
+              generateUUID(),
+              ProcedureName.STOP_CHARGING_STATION,
+              Constants.EMPTY_FROZEN_OBJECT,
             ),
-            new Promise<string>((resolve) => {
-              setTimeout(() => {
-                const message = `Timeout ${formatDurationMilliSeconds(
-                  Constants.STOP_SIMULATOR_TIMEOUT,
-                )} reached at stopping charging stations simulator`;
-                console.warn(chalk.yellow(message));
-                resolve(message);
-              }, Constants.STOP_SIMULATOR_TIMEOUT);
-            }),
-          ]);
+          );
+          await this.waitChargingStationsStopped();
         }
         await this.workerImplementation?.stop();
         this.workerImplementation = null;
@@ -218,11 +202,30 @@ export class Bootstrap extends EventEmitter {
     }
   }
 
-  public async restart(waitChargingStationsStopped?: boolean): Promise<void> {
-    await this.stop(waitChargingStationsStopped);
+  public async restart(stopChargingStations?: boolean): Promise<void> {
+    await this.stop(stopChargingStations);
     await this.start();
   }
 
+  private async waitChargingStationsStopped(): Promise<void> {
+    await Promise.race([
+      waitChargingStationEvents(
+        this,
+        ChargingStationWorkerMessageEvents.stopped,
+        this.numberOfChargingStations,
+      ),
+      new Promise<string>((resolve) => {
+        setTimeout(() => {
+          const message = `Timeout ${formatDurationMilliSeconds(
+            Constants.STOP_SIMULATOR_TIMEOUT,
+          )} reached at stopping charging stations simulator`;
+          console.warn(chalk.yellow(message));
+          resolve(message);
+        }, Constants.STOP_SIMULATOR_TIMEOUT);
+      }),
+    ]);
+  }
+
   private initializeWorkerImplementation(workerConfiguration: WorkerConfiguration): void {
     let elementsPerWorker: number | undefined;
     if (workerConfiguration?.elementsPerWorker === 'auto') {
@@ -383,17 +386,25 @@ export class Bootstrap extends EventEmitter {
     });
   }
 
-  private gracefulShutdown = (): void => {
+  private gracefulShutdown(): void {
     this.stop()
       .then(() => {
         console.info(`${chalk.green('Graceful shutdown')}`);
-        exit(exitCodes.succeeded);
+        // stop() asks for charging stations to stop by default
+        this.waitChargingStationsStopped()
+          .then(() => {
+            exit(exitCodes.succeeded);
+          })
+          .catch((error) => {
+            console.error(chalk.red('Error while waiting for charging stations to stop: '), error);
+            exit(exitCodes.gracefulShutdownError);
+          });
       })
       .catch((error) => {
         console.error(chalk.red('Error while shutdowning charging stations simulator: '), error);
         exit(exitCodes.gracefulShutdownError);
       });
-  };
+  }
 
   private logPrefix = (): string => {
     return logPrefix(' Bootstrap |');
index 133389d60b3cf18e01ad04922332121bbf0d37da..d27ea616bfe98591e9afbc8a3dff65c8f7a412df 100644 (file)
@@ -56,6 +56,7 @@ export abstract class AbstractUIServer {
   }
 
   public stop(): void {
+    this.stopHttpServer();
     this.chargingStations.clear();
   }
 
@@ -93,6 +94,12 @@ export abstract class AbstractUIServer {
     next();
   }
 
+  private stopHttpServer(): void {
+    if (this.httpServer.listening === true) {
+      this.httpServer.close();
+    }
+  }
+
   private isBasicAuthEnabled(): boolean {
     return (
       this.uiServerConfiguration.authentication?.enabled === true &&
index 872348795e4838f6dc6571e3f0650c7a2da38820..537aa308b41a0516e4b688c7f9b38124952d455f 100644 (file)
@@ -1,3 +1,5 @@
+import process from 'node:process';
+
 import chalk from 'chalk';
 
 import { logger } from './Logger';