1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import path from
'node:path';
4 import { fileURLToPath
} from
'node:url';
5 import { type Worker
, isMainThread
} from
'node:worker_threads';
7 import chalk from
'chalk';
9 import type { AbstractUIServer
} from
'./ui-server/AbstractUIServer';
10 import { UIServerFactory
} from
'./ui-server/UIServerFactory';
11 import packageJson from
'../../package.json' assert
{ type: 'json' };
12 import { BaseError
} from
'../exception';
13 import { type Storage
, StorageFactory
} from
'../performance';
15 type ChargingStationData
,
16 type ChargingStationWorkerData
,
17 type ChargingStationWorkerMessage
,
18 type ChargingStationWorkerMessageData
,
19 ChargingStationWorkerMessageEvents
,
20 type StationTemplateUrl
,
23 import { Configuration
, ErrorUtils
, Utils
, logger
} from
'../utils';
24 import { type MessageHandler
, type WorkerAbstract
, WorkerFactory
} from
'../worker';
26 const moduleName
= 'Bootstrap';
29 missingChargingStationsConfiguration
= 1,
30 noChargingStationTemplates
= 2,
33 export class Bootstrap
{
34 private static instance
: Bootstrap
| null = null;
35 public numberOfChargingStations
!: number;
36 public numberOfChargingStationTemplates
!: number;
37 private workerImplementation
: WorkerAbstract
<ChargingStationWorkerData
> | null;
38 private readonly uiServer
!: AbstractUIServer
| null;
39 private readonly storage
!: Storage
;
40 private numberOfStartedChargingStations
!: number;
41 private readonly version
: string = packageJson
.version
;
42 private initializedCounters
: boolean;
43 private started
: boolean;
44 private readonly workerScript
: string;
46 private constructor() {
47 // Enable unconditionally for now
48 ErrorUtils
.handleUnhandledRejection();
49 ErrorUtils
.handleUncaughtException();
50 this.initializedCounters
= false;
52 this.initializeCounters();
53 this.workerImplementation
= null;
54 this.workerScript
= path
.join(
55 path
.dirname(fileURLToPath(import.meta
.url
)),
56 `ChargingStationWorker${path.extname(fileURLToPath(import.meta.url))}`
58 Configuration
.getUIServer().enabled
=== true &&
59 (this.uiServer
= UIServerFactory
.getUIServerImplementation(Configuration
.getUIServer()));
60 Configuration
.getPerformanceStorage().enabled
=== true &&
61 (this.storage
= StorageFactory
.getStorage(
62 Configuration
.getPerformanceStorage().type,
63 Configuration
.getPerformanceStorage().uri
,
66 Configuration
.setConfigurationChangeCallback(async () => Bootstrap
.getInstance().restart());
69 public static getInstance(): Bootstrap
{
70 if (Bootstrap
.instance
=== null) {
71 Bootstrap
.instance
= new Bootstrap();
73 return Bootstrap
.instance
;
76 public async start(): Promise
<void> {
77 if (isMainThread
&& this.started
=== false) {
78 this.initializeCounters();
79 this.initializeWorkerImplementation();
80 await this.workerImplementation
?.start();
81 await this.storage
?.open();
82 this.uiServer
?.start();
83 // Start ChargingStation object instance in worker thread
84 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()) {
86 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
87 for (let index
= 1; index
<= nbStations
; index
++) {
88 await this.startChargingStation(index
, stationTemplateUrl
);
93 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
101 `Charging stations simulator ${
103 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
104 Configuration.workerDynamicPoolInUse()
105 ? `${Configuration.getWorker().poolMinSize?.toString()}
/`
107 }${this.workerImplementation?.size}${
108 Configuration.workerPoolInUse()
109 ? `/${Configuration.getWorker().poolMaxSize?.toString()}
`
111 } worker(s) concurrently running in '${Configuration.getWorker().processType}' mode${
112 !Utils.isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
113 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
120 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
124 public async stop(): Promise
<void> {
125 if (isMainThread
&& this.started
=== true) {
126 await this.workerImplementation
?.stop();
127 this.workerImplementation
= null;
128 this.uiServer
?.stop();
129 await this.storage
?.close();
130 this.initializedCounters
= false;
131 this.started
= false;
133 console
.error(chalk
.red('Cannot stop a not started charging stations simulator'));
137 public async restart(): Promise
<void> {
142 private initializeWorkerImplementation(): void {
143 this.workerImplementation
=== null &&
144 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
146 Configuration
.getWorker().processType
,
148 workerStartDelay
: Configuration
.getWorker().startDelay
,
149 elementStartDelay
: Configuration
.getWorker().elementStartDelay
,
150 poolMaxSize
: Configuration
.getWorker().poolMaxSize
,
151 poolMinSize
: Configuration
.getWorker().poolMinSize
,
152 elementsPerWorker
: Configuration
.getWorker().elementsPerWorker
,
154 workerChoiceStrategy
: Configuration
.getWorker().poolStrategy
,
156 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
161 private messageHandler(
162 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
165 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
173 case ChargingStationWorkerMessageEvents
.started
:
174 this.workerEventStarted(msg
.data
as ChargingStationData
);
176 case ChargingStationWorkerMessageEvents
.stopped
:
177 this.workerEventStopped(msg
.data
as ChargingStationData
);
179 case ChargingStationWorkerMessageEvents
.updated
:
180 this.workerEventUpdated(msg
.data
as ChargingStationData
);
182 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
183 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
187 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`
192 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
200 private workerEventStarted
= (data
: ChargingStationData
) => {
201 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
202 ++this.numberOfStartedChargingStations
;
204 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
205 data.stationInfo.chargingStationId
206 } (hashId: ${data.stationInfo.hashId}) started (${
207 this.numberOfStartedChargingStations
208 } started from ${this.numberOfChargingStations})`
212 private workerEventStopped
= (data
: ChargingStationData
) => {
213 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
214 --this.numberOfStartedChargingStations
;
216 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
217 data.stationInfo.chargingStationId
218 } (hashId: ${data.stationInfo.hashId}) stopped (${
219 this.numberOfStartedChargingStations
220 } started from ${this.numberOfChargingStations})`
224 private workerEventUpdated
= (data
: ChargingStationData
) => {
225 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
228 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
229 this.storage
.storePerformanceStatistics(data
) as void;
232 private initializeCounters() {
233 if (this.initializedCounters
=== false) {
234 this.numberOfChargingStationTemplates
= 0;
235 this.numberOfChargingStations
= 0;
236 const stationTemplateUrls
= Configuration
.getStationTemplateUrls();
237 if (Utils
.isNotEmptyArray(stationTemplateUrls
)) {
238 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
239 for (const stationTemplateUrl
of stationTemplateUrls
) {
240 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
244 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
246 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
248 if (this.numberOfChargingStations
=== 0) {
250 chalk
.yellow('No charging station template enabled in configuration, exiting')
252 process
.exit(exitCodes
.noChargingStationTemplates
);
254 this.numberOfStartedChargingStations
= 0;
255 this.initializedCounters
= true;
259 private async startChargingStation(
261 stationTemplateUrl
: StationTemplateUrl
263 await this.workerImplementation
?.addElement({
265 templateFile
: path
.join(
266 path
.dirname(fileURLToPath(import.meta
.url
)),
269 stationTemplateUrl
.file
274 private logPrefix
= (): string => {
275 return Utils
.logPrefix(' Bootstrap |');