1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import { EventEmitter
} from
'node:events';
4 import { dirname
, extname
, join
} from
'node:path';
5 import { exit
} from
'node:process';
6 import { fileURLToPath
} from
'node:url';
8 import chalk from
'chalk';
9 import { availableParallelism
} from
'poolifier';
11 import { waitChargingStationEvents
} from
'./Helpers';
12 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer';
13 import { UIServerFactory
} from
'./ui-server/UIServerFactory';
14 import { version
} from
'../../package.json';
15 import { BaseError
} from
'../exception';
16 import { type Storage
, StorageFactory
} from
'../performance';
18 type ChargingStationData
,
19 type ChargingStationWorkerData
,
20 type ChargingStationWorkerMessage
,
21 type ChargingStationWorkerMessageData
,
22 ChargingStationWorkerMessageEvents
,
25 type StationTemplateUrl
,
27 type StorageConfiguration
,
28 type UIServerConfiguration
,
29 type WorkerConfiguration
,
34 formatDurationMilliSeconds
,
36 handleUncaughtException
,
37 handleUnhandledRejection
,
43 import { type WorkerAbstract
, WorkerFactory
} from
'../worker';
45 const moduleName
= 'Bootstrap';
49 missingChargingStationsConfiguration
= 1,
50 noChargingStationTemplates
= 2,
51 gracefulShutdownError
= 3,
54 export class Bootstrap
extends EventEmitter
{
55 private static instance
: Bootstrap
| null = null;
56 public numberOfChargingStations
!: number;
57 public numberOfChargingStationTemplates
!: number;
58 private workerImplementation
: WorkerAbstract
<ChargingStationWorkerData
> | null;
59 private readonly uiServer
!: AbstractUIServer
| null;
60 private readonly storage
!: Storage
;
61 private numberOfStartedChargingStations
!: number;
62 private readonly version
: string = version
;
63 private initializedCounters
: boolean;
64 private started
: boolean;
65 private starting
: boolean;
66 private stopping
: boolean;
67 private readonly workerScript
: string;
69 private constructor() {
71 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
72 process
.on(signal
, this.gracefulShutdown
);
74 // Enable unconditionally for now
75 handleUnhandledRejection();
76 handleUncaughtException();
78 this.starting
= false;
79 this.stopping
= false;
80 this.initializedCounters
= false;
81 this.initializeCounters();
82 this.workerImplementation
= null;
83 this.workerScript
= join(
84 dirname(fileURLToPath(import.meta
.url
)),
85 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`,
87 const uiServerConfiguration
= Configuration
.getConfigurationSection
<UIServerConfiguration
>(
88 ConfigurationSection
.uiServer
,
90 uiServerConfiguration
.enabled
=== true &&
91 (this.uiServer
= UIServerFactory
.getUIServerImplementation(uiServerConfiguration
));
92 const performanceStorageConfiguration
=
93 Configuration
.getConfigurationSection
<StorageConfiguration
>(
94 ConfigurationSection
.performanceStorage
,
96 performanceStorageConfiguration
.enabled
=== true &&
97 (this.storage
= StorageFactory
.getStorage(
98 performanceStorageConfiguration
.type!,
99 performanceStorageConfiguration
.uri
!,
102 // FIXME: Disabled until the spurious configuration file change detection on MacOS is identified
103 // Configuration.configurationChangeCallback = async () => Bootstrap.getInstance().restart(false);
106 public static getInstance(): Bootstrap
{
107 if (Bootstrap
.instance
=== null) {
108 Bootstrap
.instance
= new Bootstrap();
110 return Bootstrap
.instance
;
113 public async start(): Promise
<void> {
114 if (this.started
=== false) {
115 if (this.starting
=== false) {
116 this.starting
= true;
117 this.initializeCounters();
118 const workerConfiguration
= Configuration
.getConfigurationSection
<WorkerConfiguration
>(
119 ConfigurationSection
.worker
,
121 this.initializeWorkerImplementation(workerConfiguration
);
122 await this.workerImplementation
?.start();
123 await this.storage
?.open();
124 this.uiServer
?.start();
125 // Start ChargingStation object instance in worker thread
126 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()!) {
128 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
129 for (let index
= 1; index
<= nbStations
; index
++) {
130 await this.startChargingStation(index
, stationTemplateUrl
);
135 `Error at starting charging station with template file ${stationTemplateUrl.file}: `,
143 `Charging stations simulator ${
145 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
146 Configuration.workerDynamicPoolInUse()
147 ? `${workerConfiguration.poolMinSize?.toString()}
/`
149 }${this.workerImplementation?.size}${
150 Configuration.workerPoolInUse()
151 ? `/${workerConfiguration.poolMaxSize?.toString()}
`
153 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
154 !isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
155 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
160 Configuration
.workerDynamicPoolInUse() &&
163 'Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using fixed pool or worker set mode instead',
166 console
.info(chalk
.green('Worker set/pool information:'), this.workerImplementation
?.info
);
168 this.starting
= false;
170 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
173 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
177 public async stop(waitChargingStationsStopped
= true): Promise
<void> {
178 if (this.started
=== true) {
179 if (this.stopping
=== false) {
180 this.stopping
= true;
181 await this.uiServer
?.sendInternalRequest(
182 this.uiServer
.buildProtocolRequest(
184 ProcedureName
.STOP_CHARGING_STATION
,
185 Constants
.EMPTY_FROZEN_OBJECT
,
188 if (waitChargingStationsStopped
=== true) {
190 waitChargingStationEvents(
192 ChargingStationWorkerMessageEvents
.stopped
,
193 this.numberOfChargingStations
,
195 new Promise
<string>((resolve
) => {
197 const message
= `Timeout ${formatDurationMilliSeconds(
198 Constants.STOP_SIMULATOR_TIMEOUT,
199 )} reached at stopping charging stations simulator`;
200 console
.warn(chalk
.yellow(message
));
202 }, Constants
.STOP_SIMULATOR_TIMEOUT
);
206 await this.workerImplementation
?.stop();
207 this.workerImplementation
= null;
208 this.uiServer
?.stop();
209 await this.storage
?.close();
210 this.resetCounters();
211 this.initializedCounters
= false;
212 this.started
= false;
213 this.stopping
= false;
215 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
218 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
222 public async restart(waitChargingStationsStopped
?: boolean): Promise
<void> {
223 await this.stop(waitChargingStationsStopped
);
227 private initializeWorkerImplementation(workerConfiguration
: WorkerConfiguration
): void {
228 let elementsPerWorker
: number | undefined;
229 if (workerConfiguration
?.elementsPerWorker
=== 'auto') {
231 this.numberOfChargingStations
> availableParallelism()
232 ? Math.round(this.numberOfChargingStations
/ (availableParallelism() * 1.5))
235 this.workerImplementation
=== null &&
236 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
238 workerConfiguration
.processType
!,
240 workerStartDelay
: workerConfiguration
.startDelay
,
241 elementStartDelay
: workerConfiguration
.elementStartDelay
,
242 poolMaxSize
: workerConfiguration
.poolMaxSize
!,
243 poolMinSize
: workerConfiguration
.poolMinSize
!,
244 elementsPerWorker
: elementsPerWorker
?? (workerConfiguration
.elementsPerWorker
as number),
246 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
252 private messageHandler(
253 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>,
256 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
264 case ChargingStationWorkerMessageEvents
.started
:
265 this.workerEventStarted(msg
.data
as ChargingStationData
);
266 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
268 case ChargingStationWorkerMessageEvents
.stopped
:
269 this.workerEventStopped(msg
.data
as ChargingStationData
);
270 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
272 case ChargingStationWorkerMessageEvents
.updated
:
273 this.workerEventUpdated(msg
.data
as ChargingStationData
);
274 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
276 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
277 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
279 ChargingStationWorkerMessageEvents
.performanceStatistics
,
280 msg
.data
as Statistics
,
283 case ChargingStationWorkerMessageEvents
.startWorkerElementError
:
285 `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`,
288 this.emit(ChargingStationWorkerMessageEvents
.startWorkerElementError
, msg
.data
);
290 case ChargingStationWorkerMessageEvents
.startedWorkerElement
:
294 `Unknown charging station worker event: '${
296 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`,
301 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
309 private workerEventStarted
= (data
: ChargingStationData
) => {
310 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
311 ++this.numberOfStartedChargingStations
;
313 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
314 data.stationInfo.chargingStationId
315 } (hashId: ${data.stationInfo.hashId}) started (${
316 this.numberOfStartedChargingStations
317 } started from ${this.numberOfChargingStations})`,
321 private workerEventStopped
= (data
: ChargingStationData
) => {
322 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
323 --this.numberOfStartedChargingStations
;
325 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
326 data.stationInfo.chargingStationId
327 } (hashId: ${data.stationInfo.hashId}) stopped (${
328 this.numberOfStartedChargingStations
329 } started from ${this.numberOfChargingStations})`,
333 private workerEventUpdated
= (data
: ChargingStationData
) => {
334 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
337 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
338 this.storage
.storePerformanceStatistics(data
) as void;
341 private initializeCounters() {
342 if (this.initializedCounters
=== false) {
343 this.resetCounters();
344 const stationTemplateUrls
= Configuration
.getStationTemplateUrls()!;
345 if (isNotEmptyArray(stationTemplateUrls
)) {
346 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
347 for (const stationTemplateUrl
of stationTemplateUrls
) {
348 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
352 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"),
354 exit(exitCodes
.missingChargingStationsConfiguration
);
356 if (this.numberOfChargingStations
=== 0) {
358 chalk
.yellow('No charging station template enabled in configuration, exiting'),
360 exit(exitCodes
.noChargingStationTemplates
);
362 this.initializedCounters
= true;
366 private resetCounters(): void {
367 this.numberOfChargingStationTemplates
= 0;
368 this.numberOfChargingStations
= 0;
369 this.numberOfStartedChargingStations
= 0;
372 private async startChargingStation(
374 stationTemplateUrl
: StationTemplateUrl
,
376 await this.workerImplementation
?.addElement({
379 dirname(fileURLToPath(import.meta
.url
)),
382 stationTemplateUrl
.file
,
387 private gracefulShutdown
= (): void => {
390 console
.info(`${chalk.green('Graceful shutdown')}`);
391 exit(exitCodes
.succeeded
);
394 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
395 exit(exitCodes
.gracefulShutdownError
);
399 private logPrefix
= (): string => {
400 return logPrefix(' Bootstrap |');