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
'worker_threads';
7 import chalk from
'chalk';
9 import { ChargingStationUtils
} from
'./ChargingStationUtils';
10 import { type AbstractUIServer
, UIServerFactory
} from
'./internal';
11 import { version
} from
'../../package.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
} from
'../utils/Configuration';
24 import { logger
} from
'../utils/Logger';
25 import { Utils
} from
'../utils/Utils';
26 import { type MessageHandler
, type WorkerAbstract
, WorkerFactory
} from
'../worker';
28 const moduleName
= 'Bootstrap';
31 missingChargingStationsConfiguration
= 1,
32 noChargingStationTemplates
= 2,
35 export class Bootstrap
{
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 = version
;
44 private initializedCounters
: boolean;
45 private started
: boolean;
46 private readonly workerScript
: string;
48 private constructor() {
49 // Enable unconditionally for now
50 this.logUnhandledRejection();
51 this.logUncaughtException();
52 this.initializedCounters
= false;
54 this.initializeCounters();
55 this.workerImplementation
= null;
56 this.workerScript
= path
.join(
57 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
59 `ChargingStationWorker${path.extname(fileURLToPath(import.meta.url))}`
61 Configuration
.getUIServer().enabled
=== true &&
62 (this.uiServer
= UIServerFactory
.getUIServerImplementation(Configuration
.getUIServer()));
63 Configuration
.getPerformanceStorage().enabled
=== true &&
64 (this.storage
= StorageFactory
.getStorage(
65 Configuration
.getPerformanceStorage().type,
66 Configuration
.getPerformanceStorage().uri
,
69 Configuration
.setConfigurationChangeCallback(async () => Bootstrap
.getInstance().restart());
72 public static getInstance(): Bootstrap
{
73 if (Bootstrap
.instance
=== null) {
74 Bootstrap
.instance
= new Bootstrap();
76 return Bootstrap
.instance
;
79 public async start(): Promise
<void> {
80 if (isMainThread
&& this.started
=== false) {
81 this.initializeCounters();
82 this.initializeWorkerImplementation();
83 await this.workerImplementation
?.start();
84 await this.storage
?.open();
85 this.uiServer
?.start();
86 // Start ChargingStation object instance in worker thread
87 for (const stationTemplateUrl
of Configuration
.getStationTemplateUrls()) {
89 const nbStations
= stationTemplateUrl
.numberOfStations
?? 0;
90 for (let index
= 1; index
<= nbStations
; index
++) {
91 await this.startChargingStation(index
, stationTemplateUrl
);
96 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
104 `Charging stations simulator ${
106 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
107 ChargingStationUtils.workerDynamicPoolInUse()
108 ? `${Configuration.getWorker().poolMinSize?.toString()}
/`
110 }${this.workerImplementation?.size}${
111 ChargingStationUtils.workerPoolInUse()
112 ? `/${Configuration.getWorker().poolMaxSize?.toString()}
`
114 } worker(s) concurrently running in '${Configuration.getWorker().processType}' mode${
115 !Utils.isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
116 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging
station(s
) per worker
)`
123 console
.error(chalk
.red('Cannot start an already started charging stations simulator'));
127 public async stop(): Promise
<void> {
128 if (isMainThread
&& this.started
=== true) {
129 await this.workerImplementation
?.stop();
130 this.workerImplementation
= null;
131 this.uiServer
?.stop();
132 await this.storage
?.close();
133 this.initializedCounters
= false;
134 this.started
= false;
136 console
.error(chalk
.red('Cannot stop a not started charging stations simulator'));
140 public async restart(): Promise
<void> {
145 private initializeWorkerImplementation(): void {
146 this.workerImplementation
=== null &&
147 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
149 Configuration
.getWorker().processType
,
151 workerStartDelay
: Configuration
.getWorker().startDelay
,
152 elementStartDelay
: Configuration
.getWorker().elementStartDelay
,
153 poolMaxSize
: Configuration
.getWorker().poolMaxSize
,
154 poolMinSize
: Configuration
.getWorker().poolMinSize
,
155 elementsPerWorker
: Configuration
.getWorker().elementsPerWorker
,
157 workerChoiceStrategy
: Configuration
.getWorker().poolStrategy
,
159 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
164 private messageHandler(
165 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
168 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
176 case ChargingStationWorkerMessageEvents
.STARTED
:
177 this.workerEventStarted(msg
.data
as ChargingStationData
);
179 case ChargingStationWorkerMessageEvents
.STOPPED
:
180 this.workerEventStopped(msg
.data
as ChargingStationData
);
182 case ChargingStationWorkerMessageEvents
.UPDATED
:
183 this.workerEventUpdated(msg
.data
as ChargingStationData
);
185 case ChargingStationWorkerMessageEvents
.PERFORMANCE_STATISTICS
:
186 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
190 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`
195 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
203 private workerEventStarted
= (data
: ChargingStationData
) => {
204 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
205 ++this.numberOfStartedChargingStations
;
207 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
208 data.stationInfo.chargingStationId
209 } (hashId: ${data.stationInfo.hashId}) started (${
210 this.numberOfStartedChargingStations
211 } started from ${this.numberOfChargingStations})`
215 private workerEventStopped
= (data
: ChargingStationData
) => {
216 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
217 --this.numberOfStartedChargingStations
;
219 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
220 data.stationInfo.chargingStationId
221 } (hashId: ${data.stationInfo.hashId}) stopped (${
222 this.numberOfStartedChargingStations
223 } started from ${this.numberOfChargingStations})`
227 private workerEventUpdated
= (data
: ChargingStationData
) => {
228 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
231 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
232 this.storage
.storePerformanceStatistics(data
) as void;
235 private initializeCounters() {
236 if (this.initializedCounters
=== false) {
237 this.numberOfChargingStationTemplates
= 0;
238 this.numberOfChargingStations
= 0;
239 const stationTemplateUrls
= Configuration
.getStationTemplateUrls();
240 if (Utils
.isNotEmptyArray(stationTemplateUrls
)) {
241 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
242 stationTemplateUrls
.forEach((stationTemplateUrl
) => {
243 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
247 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
249 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
251 if (this.numberOfChargingStations
=== 0) {
253 chalk
.yellow('No charging station template enabled in configuration, exiting')
255 process
.exit(exitCodes
.noChargingStationTemplates
);
257 this.numberOfStartedChargingStations
= 0;
258 this.initializedCounters
= true;
262 private logUncaughtException(): void {
263 process
.on('uncaughtException', (error
: Error) => {
264 console
.error(chalk
.red('Uncaught exception: '), error
);
268 private logUnhandledRejection(): void {
269 process
.on('unhandledRejection', (reason
: unknown
) => {
270 console
.error(chalk
.red('Unhandled rejection: '), reason
);
274 private async startChargingStation(
276 stationTemplateUrl
: StationTemplateUrl
278 await this.workerImplementation
?.addElement({
280 templateFile
: path
.join(
281 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
284 stationTemplateUrl
.file
289 private logPrefix
= (): string => {
290 return Utils
.logPrefix(' Bootstrap |');