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
,
25 import { Configuration
, Constants
, ErrorUtils
, Utils
, logger
} from
'../utils';
26 import { type MessageHandler
, type WorkerAbstract
, WorkerFactory
} from
'../worker';
28 const moduleName
= 'Bootstrap';
31 missingChargingStationsConfiguration
= 1,
32 noChargingStationTemplates
= 2,
35 export class Bootstrap
extends EventEmitter
{
36 private static instance
: Bootstrap
| null = null;
37 public numberOfChargingStations
!: number;
38 public numberOfChargingStationTemplates
!: number;
39 private workerImplementation
: WorkerAbstract
<ChargingStationWorkerData
> | null;
40 private readonly uiServer
!: AbstractUIServer
| null;
41 private readonly storage
!: Storage
;
42 private numberOfStartedChargingStations
!: number;
43 private readonly version
: string = packageJson
.version
;
44 private initializedCounters
: boolean;
45 private started
: boolean;
46 private readonly workerScript
: string;
48 private constructor() {
50 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
51 process
.on(signal
, this.gracefulShutdown
);
53 // Enable unconditionally for now
54 ErrorUtils
.handleUnhandledRejection();
55 ErrorUtils
.handleUncaughtException();
56 this.initializedCounters
= false;
58 this.initializeCounters();
59 this.workerImplementation
= null;
60 this.workerScript
= path
.join(
61 path
.dirname(fileURLToPath(import.meta
.url
)),
62 `ChargingStationWorker${path.extname(fileURLToPath(import.meta.url))}`
64 Configuration
.getUIServer().enabled
=== true &&
65 (this.uiServer
= UIServerFactory
.getUIServerImplementation(Configuration
.getUIServer()));
66 Configuration
.getPerformanceStorage().enabled
=== true &&
67 (this.storage
= StorageFactory
.getStorage(
68 Configuration
.getPerformanceStorage().type,
69 Configuration
.getPerformanceStorage().uri
,
72 Configuration
.setConfigurationChangeCallback(async () => Bootstrap
.getInstance().restart());
75 public static getInstance(): Bootstrap
{
76 if (Bootstrap
.instance
=== null) {
77 Bootstrap
.instance
= new Bootstrap();
79 return Bootstrap
.instance
;
82 public async start(): Promise
<void> {
83 if (isMainThread
&& this.started
=== false) {
84 this.initializeCounters();
85 this.initializeWorkerImplementation();
86 await this.workerImplementation
?.start();
87 await this.storage
?.open();
88 this.uiServer
?.start();
89 // Start ChargingStation object instance in worker thread
90 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()) {
92 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
93 for (let index
= 1; index
<= nbStations
; index
++) {
94 await this.startChargingStation(index
, stationTemplateUrl
);
99 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
107 `Charging stations simulator ${
109 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
110 Configuration.workerDynamicPoolInUse()
111 ? `${Configuration.getWorker().poolMinSize?.toString()}
/`
113 }${this.workerImplementation?.size}${
114 Configuration.workerPoolInUse()
115 ? `/${Configuration.getWorker().poolMaxSize?.toString()}
`
117 } worker(s) concurrently running in '${Configuration.getWorker().processType}' mode${
118 !Utils.isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
119 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
126 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
130 public async stop(): Promise
<void> {
131 if (isMainThread
&& this.started
=== true) {
132 await this.uiServer
?.sendBroadcastChannelRequest(
133 this.uiServer
.buildProtocolRequest(
134 Utils
.generateUUID(),
135 ProcedureName
.STOP_CHARGING_STATION
,
136 Constants
.EMPTY_FREEZED_OBJECT
139 await this.waitForChargingStationsStopped();
140 await this.workerImplementation
?.stop();
141 this.workerImplementation
= null;
142 this.uiServer
?.stop();
143 await this.storage
?.close();
144 this.initializedCounters
= false;
145 this.started
= false;
147 console
.error(chalk
.red('Cannot stop a not started charging stations simulator'));
151 public async restart(): Promise
<void> {
156 private initializeWorkerImplementation(): void {
157 this.workerImplementation
=== null &&
158 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
160 Configuration
.getWorker().processType
,
162 workerStartDelay
: Configuration
.getWorker().startDelay
,
163 elementStartDelay
: Configuration
.getWorker().elementStartDelay
,
164 poolMaxSize
: Configuration
.getWorker().poolMaxSize
,
165 poolMinSize
: Configuration
.getWorker().poolMinSize
,
166 elementsPerWorker
: Configuration
.getWorker().elementsPerWorker
,
168 workerChoiceStrategy
: Configuration
.getWorker().poolStrategy
,
170 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
175 private messageHandler(
176 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
179 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
187 case ChargingStationWorkerMessageEvents
.started
:
188 this.workerEventStarted(msg
.data
as ChargingStationData
);
189 this.emit(ChargingStationWorkerMessageEvents
.started
, msg
.data
as ChargingStationData
);
191 case ChargingStationWorkerMessageEvents
.stopped
:
192 this.workerEventStopped(msg
.data
as ChargingStationData
);
193 this.emit(ChargingStationWorkerMessageEvents
.stopped
, msg
.data
as ChargingStationData
);
195 case ChargingStationWorkerMessageEvents
.updated
:
196 this.workerEventUpdated(msg
.data
as ChargingStationData
);
197 this.emit(ChargingStationWorkerMessageEvents
.updated
, msg
.data
as ChargingStationData
);
199 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
200 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
202 ChargingStationWorkerMessageEvents
.performanceStatistics
,
203 msg
.data
as Statistics
208 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`
213 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
221 private workerEventStarted
= (data
: ChargingStationData
) => {
222 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
223 ++this.numberOfStartedChargingStations
;
225 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
226 data.stationInfo.chargingStationId
227 } (hashId: ${data.stationInfo.hashId}) started (${
228 this.numberOfStartedChargingStations
229 } started from ${this.numberOfChargingStations})`
233 private workerEventStopped
= (data
: ChargingStationData
) => {
234 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
235 --this.numberOfStartedChargingStations
;
237 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
238 data.stationInfo.chargingStationId
239 } (hashId: ${data.stationInfo.hashId}) stopped (${
240 this.numberOfStartedChargingStations
241 } started from ${this.numberOfChargingStations})`
245 private workerEventUpdated
= (data
: ChargingStationData
) => {
246 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
249 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
250 this.storage
.storePerformanceStatistics(data
) as void;
253 private initializeCounters() {
254 if (this.initializedCounters
=== false) {
255 this.numberOfChargingStationTemplates
= 0;
256 this.numberOfChargingStations
= 0;
257 const stationTemplateUrls
= Configuration
.getStationTemplateUrls();
258 if (Utils
.isNotEmptyArray(stationTemplateUrls
)) {
259 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
260 for (const stationTemplateUrl
of stationTemplateUrls
) {
261 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
265 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
267 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
269 if (this.numberOfChargingStations
=== 0) {
271 chalk
.yellow('No charging station template enabled in configuration, exiting')
273 process
.exit(exitCodes
.noChargingStationTemplates
);
275 this.numberOfStartedChargingStations
= 0;
276 this.initializedCounters
= true;
280 private async startChargingStation(
282 stationTemplateUrl
: StationTemplateUrl
284 await this.workerImplementation
?.addElement({
286 templateFile
: path
.join(
287 path
.dirname(fileURLToPath(import.meta
.url
)),
290 stationTemplateUrl
.file
295 private gracefulShutdown
= (): void => {
296 console
.info(`${chalk.green('Graceful shutdown')}`);
302 console
.error(chalk
.red('Error while stopping charging stations simulator:'), error
);
307 private waitForChargingStationsStopped
= async (): Promise
<void> => {
308 return new Promise((resolve
) => {
309 let stoppedEvents
= 0;
310 this.on(ChargingStationWorkerMessageEvents
.stopped
, () => {
312 if (stoppedEvents
=== this.numberOfChargingStations
) {
319 private logPrefix
= (): string => {
320 return Utils
.logPrefix(' Bootstrap |');