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
,
21 type StationTemplateUrl
,
24 import { Configuration
, Constants
, ErrorUtils
, Utils
, logger
} from
'../utils';
25 import { type MessageHandler
, type WorkerAbstract
, WorkerFactory
} from
'../worker';
27 const moduleName
= 'Bootstrap';
30 missingChargingStationsConfiguration
= 1,
31 noChargingStationTemplates
= 2,
34 export class Bootstrap
{
35 private static instance
: Bootstrap
| null = null;
36 public numberOfChargingStations
!: number;
37 public numberOfChargingStationTemplates
!: number;
38 private workerImplementation
: WorkerAbstract
<ChargingStationWorkerData
> | null;
39 private readonly uiServer
!: AbstractUIServer
| null;
40 private readonly storage
!: Storage
;
41 private numberOfStartedChargingStations
!: number;
42 private readonly version
: string = packageJson
.version
;
43 private initializedCounters
: boolean;
44 private started
: boolean;
45 private readonly workerScript
: string;
47 private constructor() {
48 for (const signal
of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
49 process
.on(signal
, () => {
50 this.gracefulShutdown().catch(Constants
.EMPTY_FUNCTION
);
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 Utils
.generateUUID(),
134 ProcedureName
.STOP_CHARGING_STATION
,
135 Constants
.EMPTY_FREEZED_OBJECT
137 await this.workerImplementation
?.stop();
138 this.workerImplementation
= null;
139 this.uiServer
?.stop();
140 await this.storage
?.close();
141 this.initializedCounters
= false;
142 this.started
= false;
144 console
.error(chalk
.red('Cannot stop a not started charging stations simulator'));
148 public async restart(): Promise
<void> {
153 private initializeWorkerImplementation(): void {
154 this.workerImplementation
=== null &&
155 (this.workerImplementation
= WorkerFactory
.getWorkerImplementation
<ChargingStationWorkerData
>(
157 Configuration
.getWorker().processType
,
159 workerStartDelay
: Configuration
.getWorker().startDelay
,
160 elementStartDelay
: Configuration
.getWorker().elementStartDelay
,
161 poolMaxSize
: Configuration
.getWorker().poolMaxSize
,
162 poolMinSize
: Configuration
.getWorker().poolMinSize
,
163 elementsPerWorker
: Configuration
.getWorker().elementsPerWorker
,
165 workerChoiceStrategy
: Configuration
.getWorker().poolStrategy
,
167 messageHandler
: this.messageHandler
.bind(this) as MessageHandler
<Worker
>,
172 private messageHandler(
173 msg
: ChargingStationWorkerMessage
<ChargingStationWorkerMessageData
>
176 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
184 case ChargingStationWorkerMessageEvents
.started
:
185 this.workerEventStarted(msg
.data
as ChargingStationData
);
187 case ChargingStationWorkerMessageEvents
.stopped
:
188 this.workerEventStopped(msg
.data
as ChargingStationData
);
190 case ChargingStationWorkerMessageEvents
.updated
:
191 this.workerEventUpdated(msg
.data
as ChargingStationData
);
193 case ChargingStationWorkerMessageEvents
.performanceStatistics
:
194 this.workerEventPerformanceStatistics(msg
.data
as Statistics
);
198 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`
203 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
211 private workerEventStarted
= (data
: ChargingStationData
) => {
212 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
213 ++this.numberOfStartedChargingStations
;
215 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
216 data.stationInfo.chargingStationId
217 } (hashId: ${data.stationInfo.hashId}) started (${
218 this.numberOfStartedChargingStations
219 } started from ${this.numberOfChargingStations})`
223 private workerEventStopped
= (data
: ChargingStationData
) => {
224 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
225 --this.numberOfStartedChargingStations
;
227 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
228 data.stationInfo.chargingStationId
229 } (hashId: ${data.stationInfo.hashId}) stopped (${
230 this.numberOfStartedChargingStations
231 } started from ${this.numberOfChargingStations})`
235 private workerEventUpdated
= (data
: ChargingStationData
) => {
236 this.uiServer
?.chargingStations
.set(data
.stationInfo
.hashId
, data
);
239 private workerEventPerformanceStatistics
= (data
: Statistics
) => {
240 this.storage
.storePerformanceStatistics(data
) as void;
243 private initializeCounters() {
244 if (this.initializedCounters
=== false) {
245 this.numberOfChargingStationTemplates
= 0;
246 this.numberOfChargingStations
= 0;
247 const stationTemplateUrls
= Configuration
.getStationTemplateUrls();
248 if (Utils
.isNotEmptyArray(stationTemplateUrls
)) {
249 this.numberOfChargingStationTemplates
= stationTemplateUrls
.length
;
250 for (const stationTemplateUrl
of stationTemplateUrls
) {
251 this.numberOfChargingStations
+= stationTemplateUrl
.numberOfStations
?? 0;
255 chalk
.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
257 process
.exit(exitCodes
.missingChargingStationsConfiguration
);
259 if (this.numberOfChargingStations
=== 0) {
261 chalk
.yellow('No charging station template enabled in configuration, exiting')
263 process
.exit(exitCodes
.noChargingStationTemplates
);
265 this.numberOfStartedChargingStations
= 0;
266 this.initializedCounters
= true;
270 private async startChargingStation(
272 stationTemplateUrl
: StationTemplateUrl
274 await this.workerImplementation
?.addElement({
276 templateFile
: path
.join(
277 path
.dirname(fileURLToPath(import.meta
.url
)),
280 stationTemplateUrl
.file
285 private gracefulShutdown
= async (): Promise
<void> => {
286 console
.info(`${chalk.green('Graceful shutdown')}`);
295 private logPrefix
= (): string => {
296 return Utils
.logPrefix(' Bootstrap |');