1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import { EventEmitter
} from
'node:events';
4 import path from
'node:path';
5 import { fileURLToPath
} from
'node:url';
6 import { type Worker
, isMainThread
} from
'node:worker_threads';
8 import chalk from
'chalk';
10 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer';
11 import { UIServerFactory
} from
'./ui-server/UIServerFactory';
12 import packageJson from
'../../package.json' assert
{ type: 'json' };
13 import { BaseError
} from
'../exception';
14 import { type Storage
, StorageFactory
} from
'../performance';
16 type ChargingStationData
,
17 type ChargingStationWorkerData
,
18 type ChargingStationWorkerMessage
,
19 type ChargingStationWorkerMessageData
,
20 ChargingStationWorkerMessageEvents
,
22 type StationTemplateUrl
,
29 handleUncaughtException
,
30 handleUnhandledRejection
,
33 import { type MessageHandler
, type WorkerAbstract
, WorkerFactory
} from
'../worker';
35 const moduleName
= 'Bootstrap';
38 missingChargingStationsConfiguration
= 1,
39 noChargingStationTemplates
= 2,
42 export class Bootstrap
extends EventEmitter
{
43 private static instance
: Bootstrap
| null = null;
44 public numberOfChargingStations
!: number;
45 public numberOfChargingStationTemplates
!: number;
46 private workerImplementation
: WorkerAbstract
<ChargingStationWorkerData
> | null;
47 private readonly uiServer
!: AbstractUIServer
| null;
48 private readonly storage
!: Storage
;
49 private numberOfStartedChargingStations
!: number;
50 private readonly version
: string = packageJson
.version
;
51 private initializedCounters
: boolean;
52 private started
: boolean;
53 private starting
: boolean;
54 private stopping
: boolean;
55 private readonly workerScript
: string;
57 private constructor() {
59 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
60 process
.on(signal
, this.gracefulShutdown
);
62 // Enable unconditionally for now
63 handleUnhandledRejection();
64 handleUncaughtException();
66 this.starting
= false;
67 this.stopping
= false;
68 this.initializedCounters
= false;
69 this.initializeCounters();
70 this.workerImplementation
= null;
71 this.workerScript
= path
.join(
72 path
.dirname(fileURLToPath(import.meta
.url
)),
73 `ChargingStationWorker${path.extname(fileURLToPath(import.meta.url))}`
75 Configuration
.getUIServer().enabled
=== true &&
76 (this.uiServer
= UIServerFactory
.getUIServerImplementation(Configuration
.getUIServer()));
77 Configuration
.getPerformanceStorage().enabled
=== true &&
78 (this.storage
= StorageFactory
.getStorage(
79 Configuration
.getPerformanceStorage().type,
80 Configuration
.getPerformanceStorage().uri
,
83 Configuration
.setConfigurationChangeCallback(async () => Bootstrap
.getInstance().restart());
86 public static getInstance(): Bootstrap
{
87 if (Bootstrap
.instance
=== null) {
88 Bootstrap
.instance
= new Bootstrap();
90 return Bootstrap
.instance
;
93 public async start(): Promise
<void> {
95 throw new Error('Cannot start charging stations simulator from worker thread');
97 if (this.started
=== false) {
98 if (this.starting
=== false) {
100 this.initializeCounters();
101 this.initializeWorkerImplementation();
102 await this.workerImplementation
?.start();
103 await this.storage
?.open();
104 this.uiServer
?.start();
105 // Start ChargingStation object instance in worker thread
106 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()) {
108 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
109 for (let index
= 1; index
<= nbStations
; index
++) {
110 await this.startChargingStation(index
, stationTemplateUrl
);
115 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
123 `Charging stations simulator ${
125 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
126 Configuration.workerDynamicPoolInUse()
127 ? `${Configuration.getWorker().poolMinSize?.toString()}
/`
129 }${this.workerImplementation?.size}${
130 Configuration.workerPoolInUse()
131 ? `/${Configuration.getWorker().poolMaxSize?.toString()}
`
133 } worker(s) concurrently running in '${Configuration.getWorker().processType}' mode${
134 !Utils.isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
135 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
141 this.starting
= false;
143 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
146 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
150 public async stop(): Promise
<void> {
152 throw new Error('Cannot stop charging stations simulator from worker thread');
154 if (this.started
=== true) {
155 if (this.stopping
=== false) {
156 this.stopping
= true;
157 await this.uiServer
?.sendInternalRequest(
158 this.uiServer
.buildProtocolRequest(
159 Utils
.generateUUID(),
160 ProcedureName
.STOP_CHARGING_STATION
,
161 Constants
.EMPTY_FREEZED_OBJECT
164 await this.waitForChargingStationsStopped();
165 await this.workerImplementation
?.stop();
166 this.workerImplementation
= null;
167 this.uiServer
?.stop();
168 await this.storage
?.close();
169 this.resetCounters();
170 this.initializedCounters
= false;
171 this.started
= false;
172 this.stopping
= false;
174 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
177 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
181 public async restart(): Promise
<void> {
186 private initializeWorkerImplementation(): void {
187 this.workerImplementation
=== null &&
188 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
190 Configuration
.getWorker().processType
,
192 workerStartDelay
: Configuration
.getWorker().startDelay
,
193 elementStartDelay
: Configuration
.getWorker().elementStartDelay
,
194 poolMaxSize
: Configuration
.getWorker().poolMaxSize
,
195 poolMinSize
: Configuration
.getWorker().poolMinSize
,
196 elementsPerWorker
: Configuration
.getWorker().elementsPerWorker
,
198 workerChoiceStrategy
: Configuration
.getWorker().poolStrategy
,
200 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
205 private messageHandler(
206 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
209 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
217 case ChargingStationWorkerMessageEvents
.started
:
218 this.workerEventStarted(msg
.data
as ChargingStationData
);
219 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
221 case ChargingStationWorkerMessageEvents
.stopped
:
222 this.workerEventStopped(msg
.data
as ChargingStationData
);
223 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
225 case ChargingStationWorkerMessageEvents
.updated
:
226 this.workerEventUpdated(msg
.data
as ChargingStationData
);
227 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
229 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
230 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
232 ChargingStationWorkerMessageEvents
.performanceStatistics
,
233 msg
.data
as Statistics
238 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`
243 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
251 private workerEventStarted
= (data
: ChargingStationData
) => {
252 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
253 ++this.numberOfStartedChargingStations
;
255 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
256 data.stationInfo.chargingStationId
257 } (hashId: ${data.stationInfo.hashId}) started (${
258 this.numberOfStartedChargingStations
259 } started from ${this.numberOfChargingStations})`
263 private workerEventStopped
= (data
: ChargingStationData
) => {
264 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
265 --this.numberOfStartedChargingStations
;
267 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
268 data.stationInfo.chargingStationId
269 } (hashId: ${data.stationInfo.hashId}) stopped (${
270 this.numberOfStartedChargingStations
271 } started from ${this.numberOfChargingStations})`
275 private workerEventUpdated
= (data
: ChargingStationData
) => {
276 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
279 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
280 this.storage
.storePerformanceStatistics(data
) as void;
283 private initializeCounters() {
284 if (this.initializedCounters
=== false) {
285 this.resetCounters();
286 const stationTemplateUrls
= Configuration
.getStationTemplateUrls();
287 if (Utils
.isNotEmptyArray(stationTemplateUrls
)) {
288 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
289 for (const stationTemplateUrl
of stationTemplateUrls
) {
290 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
294 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
296 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
298 if (this.numberOfChargingStations
=== 0) {
300 chalk
.yellow('No charging station template enabled in configuration, exiting')
302 process
.exit(exitCodes
.noChargingStationTemplates
);
304 this.initializedCounters
= true;
308 private resetCounters(): void {
309 this.numberOfChargingStationTemplates
= 0;
310 this.numberOfChargingStations
= 0;
311 this.numberOfStartedChargingStations
= 0;
314 private async startChargingStation(
316 stationTemplateUrl
: StationTemplateUrl
318 await this.workerImplementation
?.addElement({
320 templateFile
: path
.join(
321 path
.dirname(fileURLToPath(import.meta
.url
)),
324 stationTemplateUrl
.file
329 private gracefulShutdown
= (): void => {
330 console
.info(`${chalk.green('Graceful shutdown')}`);
336 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
341 private waitForChargingStationsStopped
= async (
342 stoppedEventsToWait
= this.numberOfStartedChargingStations
343 ): Promise
<number> => {
344 return new Promise((resolve
) => {
345 let stoppedEvents
= 0;
346 if (stoppedEventsToWait
=== 0) {
347 resolve(stoppedEvents
);
349 this.on(ChargingStationWorkerMessageEvents
.stopped
, () => {
351 if (stoppedEvents
=== stoppedEventsToWait
) {
352 resolve(stoppedEvents
);
358 private logPrefix
= (): string => {
359 return Utils
.logPrefix(' Bootstrap |');