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 { isMainThread
} from
'node:worker_threads';
8 import chalk from
'chalk';
10 import { ChargingStationUtils
} from
'./ChargingStationUtils';
11 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer';
12 import { UIServerFactory
} from
'./ui-server/UIServerFactory';
13 import packageJson from
'../../package.json' assert
{ type: 'json' };
14 import { BaseError
} from
'../exception';
15 import { type Storage
, StorageFactory
} from
'../performance';
17 type ChargingStationData
,
18 type ChargingStationWorkerData
,
19 type ChargingStationWorkerMessage
,
20 type ChargingStationWorkerMessageData
,
21 ChargingStationWorkerMessageEvents
,
23 type StationTemplateUrl
,
30 handleUncaughtException
,
31 handleUnhandledRejection
,
34 import { type WorkerAbstract
, WorkerFactory
} from
'../worker';
36 const moduleName
= 'Bootstrap';
39 missingChargingStationsConfiguration
= 1,
40 noChargingStationTemplates
= 2,
43 export class Bootstrap
extends EventEmitter
{
44 private static instance
: Bootstrap
| null = null;
45 public numberOfChargingStations
!: number;
46 public numberOfChargingStationTemplates
!: number;
47 private workerImplementation
: WorkerAbstract
<ChargingStationWorkerData
> | null;
48 private readonly uiServer
!: AbstractUIServer
| null;
49 private readonly storage
!: Storage
;
50 private numberOfStartedChargingStations
!: number;
51 private readonly version
: string = packageJson
.version
;
52 private initializedCounters
: boolean;
53 private started
: boolean;
54 private starting
: boolean;
55 private stopping
: boolean;
56 private readonly workerScript
: string;
58 private constructor() {
60 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
61 process
.on(signal
, this.gracefulShutdown
);
63 // Enable unconditionally for now
64 handleUnhandledRejection();
65 handleUncaughtException();
67 this.starting
= false;
68 this.stopping
= false;
69 this.initializedCounters
= false;
70 this.initializeCounters();
71 this.workerImplementation
= null;
72 this.workerScript
= path
.join(
73 path
.dirname(fileURLToPath(import.meta
.url
)),
74 `ChargingStationWorker${path.extname(fileURLToPath(import.meta.url))}`
76 Configuration
.getUIServer().enabled
=== true &&
77 (this.uiServer
= UIServerFactory
.getUIServerImplementation(Configuration
.getUIServer()));
78 Configuration
.getPerformanceStorage().enabled
=== true &&
79 (this.storage
= StorageFactory
.getStorage(
80 Configuration
.getPerformanceStorage().type,
81 Configuration
.getPerformanceStorage().uri
,
84 Configuration
.setConfigurationChangeCallback(async () => Bootstrap
.getInstance().restart());
87 public static getInstance(): Bootstrap
{
88 if (Bootstrap
.instance
=== null) {
89 Bootstrap
.instance
= new Bootstrap();
91 return Bootstrap
.instance
;
94 public async start(): Promise
<void> {
96 throw new Error('Cannot start charging stations simulator from worker thread');
98 if (this.started
=== false) {
99 if (this.starting
=== false) {
100 this.starting
= true;
101 this.initializeCounters();
102 this.initializeWorkerImplementation();
103 await this.workerImplementation
?.start();
104 await this.storage
?.open();
105 this.uiServer
?.start();
106 // Start ChargingStation object instance in worker thread
107 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()) {
109 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
110 for (let index
= 1; index
<= nbStations
; index
++) {
111 await this.startChargingStation(index
, stationTemplateUrl
);
116 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
124 `Charging stations simulator ${
126 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
127 Configuration.workerDynamicPoolInUse()
128 ? `${Configuration.getWorker().poolMinSize?.toString()}
/`
130 }${this.workerImplementation?.size}${
131 Configuration.workerPoolInUse()
132 ? `/${Configuration.getWorker().poolMaxSize?.toString()}
`
134 } worker(s) concurrently running in '${Configuration.getWorker().processType}' mode${
135 !Utils.isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
136 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
142 this.starting
= false;
144 console
.error(chalk
.red('Cannot start an already starting charging stations simulator'));
147 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
151 public async stop(): Promise
<void> {
153 throw new Error('Cannot stop charging stations simulator from worker thread');
155 if (this.started
=== true) {
156 if (this.stopping
=== false) {
157 this.stopping
= true;
158 await this.uiServer
?.sendInternalRequest(
159 this.uiServer
.buildProtocolRequest(
160 Utils
.generateUUID(),
161 ProcedureName
.STOP_CHARGING_STATION
,
162 Constants
.EMPTY_FREEZED_OBJECT
165 await ChargingStationUtils
.waitForChargingStationEvents(
167 ChargingStationWorkerMessageEvents
.stopped
,
168 this.numberOfChargingStations
170 await this.workerImplementation
?.stop();
171 this.workerImplementation
= null;
172 this.uiServer
?.stop();
173 await this.storage
?.close();
174 this.resetCounters();
175 this.initializedCounters
= false;
176 this.started
= false;
177 this.stopping
= false;
179 console
.error(chalk
.red('Cannot stop an already stopping charging stations simulator'));
182 console
.error(chalk
.red('Cannot stop an already stopped charging stations simulator'));
186 public async restart(): Promise
<void> {
191 private initializeWorkerImplementation(): void {
192 this.workerImplementation
=== null &&
193 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
195 Configuration
.getWorker().processType
,
197 workerStartDelay
: Configuration
.getWorker().startDelay
,
198 elementStartDelay
: Configuration
.getWorker().elementStartDelay
,
199 poolMaxSize
: Configuration
.getWorker().poolMaxSize
,
200 poolMinSize
: Configuration
.getWorker().poolMinSize
,
201 elementsPerWorker
: Configuration
.getWorker().elementsPerWorker
,
203 workerChoiceStrategy
: Configuration
.getWorker().poolStrategy
,
204 messageHandler
: this.messageHandler
.bind(this) as (message
: unknown
) => void,
210 private messageHandler(
211 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
214 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
222 case ChargingStationWorkerMessageEvents
.started
:
223 this.workerEventStarted(msg
.data
as ChargingStationData
);
224 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
226 case ChargingStationWorkerMessageEvents
.stopped
:
227 this.workerEventStopped(msg
.data
as ChargingStationData
);
228 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
230 case ChargingStationWorkerMessageEvents
.updated
:
231 this.workerEventUpdated(msg
.data
as ChargingStationData
);
232 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
234 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
235 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
237 ChargingStationWorkerMessageEvents
.performanceStatistics
,
238 msg
.data
as Statistics
243 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`
248 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
256 private workerEventStarted
= (data
: ChargingStationData
) => {
257 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
258 ++this.numberOfStartedChargingStations
;
260 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
261 data.stationInfo.chargingStationId
262 } (hashId: ${data.stationInfo.hashId}) started (${
263 this.numberOfStartedChargingStations
264 } started from ${this.numberOfChargingStations})`
268 private workerEventStopped
= (data
: ChargingStationData
) => {
269 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
270 --this.numberOfStartedChargingStations
;
272 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
273 data.stationInfo.chargingStationId
274 } (hashId: ${data.stationInfo.hashId}) stopped (${
275 this.numberOfStartedChargingStations
276 } started from ${this.numberOfChargingStations})`
280 private workerEventUpdated
= (data
: ChargingStationData
) => {
281 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
284 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
285 this.storage
.storePerformanceStatistics(data
) as void;
288 private initializeCounters() {
289 if (this.initializedCounters
=== false) {
290 this.resetCounters();
291 const stationTemplateUrls
= Configuration
.getStationTemplateUrls();
292 if (Utils
.isNotEmptyArray(stationTemplateUrls
)) {
293 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
294 for (const stationTemplateUrl
of stationTemplateUrls
) {
295 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
299 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
301 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
303 if (this.numberOfChargingStations
=== 0) {
305 chalk
.yellow('No charging station template enabled in configuration, exiting')
307 process
.exit(exitCodes
.noChargingStationTemplates
);
309 this.initializedCounters
= true;
313 private resetCounters(): void {
314 this.numberOfChargingStationTemplates
= 0;
315 this.numberOfChargingStations
= 0;
316 this.numberOfStartedChargingStations
= 0;
319 private async startChargingStation(
321 stationTemplateUrl
: StationTemplateUrl
323 await this.workerImplementation
?.addElement({
325 templateFile
: path
.join(
326 path
.dirname(fileURLToPath(import.meta
.url
)),
329 stationTemplateUrl
.file
334 private gracefulShutdown
= (): void => {
335 console
.info(`${chalk.green('Graceful shutdown')}`);
341 console
.error(chalk
.red('Error while shutdowning charging stations simulator: '), error
);
346 private logPrefix
= (): string => {
347 return Utils
.logPrefix(' Bootstrap |');