fix: ensure firmware update simulation always run as async resource
[e-mobility-charging-stations-simulator.git] / src / charging-station / Bootstrap.ts
1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
2
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';
7
8 import chalk from 'chalk';
9
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';
15 import {
16 type ChargingStationData,
17 type ChargingStationWorkerData,
18 type ChargingStationWorkerMessage,
19 type ChargingStationWorkerMessageData,
20 ChargingStationWorkerMessageEvents,
21 ProcedureName,
22 type StationTemplateUrl,
23 type Statistics,
24 } from '../types';
25 import {
26 Configuration,
27 Constants,
28 Utils,
29 handleUncaughtException,
30 handleUnhandledRejection,
31 logger,
32 } from '../utils';
33 import { type MessageHandler, type WorkerAbstract, WorkerFactory } from '../worker';
34
35 const moduleName = 'Bootstrap';
36
37 enum exitCodes {
38 missingChargingStationsConfiguration = 1,
39 noChargingStationTemplates = 2,
40 }
41
42 export class Bootstrap extends EventEmitter {
43 private static instance: Bootstrap | null = null;
44 public numberOfChargingStations!: number;
45 public numberOfChargingStationTemplates!: number;
46 private workerImplementation: WorkerAbstract<ChargingStationWorkerData> | null;
47 private readonly uiServer!: AbstractUIServer | null;
48 private readonly storage!: Storage;
49 private numberOfStartedChargingStations!: number;
50 private readonly version: string = packageJson.version;
51 private initializedCounters: boolean;
52 private started: boolean;
53 private starting: boolean;
54 private stopping: boolean;
55 private readonly workerScript: string;
56
57 private constructor() {
58 super();
59 for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
60 process.on(signal, this.gracefulShutdown);
61 }
62 // Enable unconditionally for now
63 handleUnhandledRejection();
64 handleUncaughtException();
65 this.started = false;
66 this.starting = false;
67 this.stopping = false;
68 this.initializedCounters = false;
69 this.initializeCounters();
70 this.workerImplementation = null;
71 this.workerScript = path.join(
72 path.dirname(fileURLToPath(import.meta.url)),
73 `ChargingStationWorker${path.extname(fileURLToPath(import.meta.url))}`
74 );
75 Configuration.getUIServer().enabled === true &&
76 (this.uiServer = UIServerFactory.getUIServerImplementation(Configuration.getUIServer()));
77 Configuration.getPerformanceStorage().enabled === true &&
78 (this.storage = StorageFactory.getStorage(
79 Configuration.getPerformanceStorage().type,
80 Configuration.getPerformanceStorage().uri,
81 this.logPrefix()
82 ));
83 Configuration.setConfigurationChangeCallback(async () => Bootstrap.getInstance().restart());
84 }
85
86 public static getInstance(): Bootstrap {
87 if (Bootstrap.instance === null) {
88 Bootstrap.instance = new Bootstrap();
89 }
90 return Bootstrap.instance;
91 }
92
93 public async start(): Promise<void> {
94 if (!isMainThread) {
95 throw new Error('Cannot start charging stations simulator from worker thread');
96 }
97 if (this.started === false) {
98 if (this.starting === false) {
99 this.starting = true;
100 this.initializeCounters();
101 this.initializeWorkerImplementation();
102 await this.workerImplementation?.start();
103 await this.storage?.open();
104 this.uiServer?.start();
105 // Start ChargingStation object instance in worker thread
106 for (const stationTemplateUrl of Configuration.getStationTemplateUrls()) {
107 try {
108 const nbStations = stationTemplateUrl.numberOfStations ?? 0;
109 for (let index = 1; index <= nbStations; index++) {
110 await this.startChargingStation(index, stationTemplateUrl);
111 }
112 } catch (error) {
113 console.error(
114 chalk.red(
115 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
116 ),
117 error
118 );
119 }
120 }
121 console.info(
122 chalk.green(
123 `Charging stations simulator ${
124 this.version
125 } started with ${this.numberOfChargingStations.toString()} charging station(s) from ${this.numberOfChargingStationTemplates.toString()} configured charging station template(s) and ${
126 Configuration.workerDynamicPoolInUse()
127 ? `${Configuration.getWorker().poolMinSize?.toString()}/`
128 : ''
129 }${this.workerImplementation?.size}${
130 Configuration.workerPoolInUse()
131 ? `/${Configuration.getWorker().poolMaxSize?.toString()}`
132 : ''
133 } worker(s) concurrently running in '${Configuration.getWorker().processType}' mode${
134 !Utils.isNullOrUndefined(this.workerImplementation?.maxElementsPerWorker)
135 ? ` (${this.workerImplementation?.maxElementsPerWorker} charging station(s) per worker)`
136 : ''
137 }`
138 )
139 );
140 this.started = true;
141 this.starting = false;
142 } else {
143 console.error(chalk.red('Cannot start an already starting charging stations simulator'));
144 }
145 } else {
146 console.error(chalk.red('Cannot start an already started charging stations simulator'));
147 }
148 }
149
150 public async stop(): Promise<void> {
151 if (!isMainThread) {
152 throw new Error('Cannot stop charging stations simulator from worker thread');
153 }
154 if (this.started === true) {
155 if (this.stopping === false) {
156 this.stopping = true;
157 await this.uiServer?.sendInternalRequest(
158 this.uiServer.buildProtocolRequest(
159 Utils.generateUUID(),
160 ProcedureName.STOP_CHARGING_STATION,
161 Constants.EMPTY_FREEZED_OBJECT
162 )
163 );
164 await this.waitForChargingStationsStopped();
165 await this.workerImplementation?.stop();
166 this.workerImplementation = null;
167 this.uiServer?.stop();
168 await this.storage?.close();
169 this.resetCounters();
170 this.initializedCounters = false;
171 this.started = false;
172 this.stopping = false;
173 } else {
174 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'));
175 }
176 } else {
177 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'));
178 }
179 }
180
181 public async restart(): Promise<void> {
182 await this.stop();
183 await this.start();
184 }
185
186 private initializeWorkerImplementation(): void {
187 this.workerImplementation === null &&
188 (this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
189 this.workerScript,
190 Configuration.getWorker().processType,
191 {
192 workerStartDelay: Configuration.getWorker().startDelay,
193 elementStartDelay: Configuration.getWorker().elementStartDelay,
194 poolMaxSize: Configuration.getWorker().poolMaxSize,
195 poolMinSize: Configuration.getWorker().poolMinSize,
196 elementsPerWorker: Configuration.getWorker().elementsPerWorker,
197 poolOptions: {
198 workerChoiceStrategy: Configuration.getWorker().poolStrategy,
199 },
200 messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
201 }
202 ));
203 }
204
205 private messageHandler(
206 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
207 ): void {
208 // logger.debug(
209 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
210 // msg,
211 // null,
212 // 2
213 // )}`
214 // );
215 try {
216 switch (msg.id) {
217 case ChargingStationWorkerMessageEvents.started:
218 this.workerEventStarted(msg.data as ChargingStationData);
219 this.emit(ChargingStationWorkerMessageEvents.started, msg.data as ChargingStationData);
220 break;
221 case ChargingStationWorkerMessageEvents.stopped:
222 this.workerEventStopped(msg.data as ChargingStationData);
223 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data as ChargingStationData);
224 break;
225 case ChargingStationWorkerMessageEvents.updated:
226 this.workerEventUpdated(msg.data as ChargingStationData);
227 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data as ChargingStationData);
228 break;
229 case ChargingStationWorkerMessageEvents.performanceStatistics:
230 this.workerEventPerformanceStatistics(msg.data as Statistics);
231 this.emit(
232 ChargingStationWorkerMessageEvents.performanceStatistics,
233 msg.data as Statistics
234 );
235 break;
236 default:
237 throw new BaseError(
238 `Unknown event type: '${msg.id}' for data: ${JSON.stringify(msg.data, null, 2)}`
239 );
240 }
241 } catch (error) {
242 logger.error(
243 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
244 msg.id
245 }' event:`,
246 error
247 );
248 }
249 }
250
251 private workerEventStarted = (data: ChargingStationData) => {
252 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
253 ++this.numberOfStartedChargingStations;
254 logger.info(
255 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
256 data.stationInfo.chargingStationId
257 } (hashId: ${data.stationInfo.hashId}) started (${
258 this.numberOfStartedChargingStations
259 } started from ${this.numberOfChargingStations})`
260 );
261 };
262
263 private workerEventStopped = (data: ChargingStationData) => {
264 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
265 --this.numberOfStartedChargingStations;
266 logger.info(
267 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
268 data.stationInfo.chargingStationId
269 } (hashId: ${data.stationInfo.hashId}) stopped (${
270 this.numberOfStartedChargingStations
271 } started from ${this.numberOfChargingStations})`
272 );
273 };
274
275 private workerEventUpdated = (data: ChargingStationData) => {
276 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data);
277 };
278
279 private workerEventPerformanceStatistics = (data: Statistics) => {
280 this.storage.storePerformanceStatistics(data) as void;
281 };
282
283 private initializeCounters() {
284 if (this.initializedCounters === false) {
285 this.resetCounters();
286 const stationTemplateUrls = Configuration.getStationTemplateUrls();
287 if (Utils.isNotEmptyArray(stationTemplateUrls)) {
288 this.numberOfChargingStationTemplates = stationTemplateUrls.length;
289 for (const stationTemplateUrl of stationTemplateUrls) {
290 this.numberOfChargingStations += stationTemplateUrl.numberOfStations ?? 0;
291 }
292 } else {
293 console.warn(
294 chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting")
295 );
296 process.exit(exitCodes.missingChargingStationsConfiguration);
297 }
298 if (this.numberOfChargingStations === 0) {
299 console.warn(
300 chalk.yellow('No charging station template enabled in configuration, exiting')
301 );
302 process.exit(exitCodes.noChargingStationTemplates);
303 }
304 this.initializedCounters = true;
305 }
306 }
307
308 private resetCounters(): void {
309 this.numberOfChargingStationTemplates = 0;
310 this.numberOfChargingStations = 0;
311 this.numberOfStartedChargingStations = 0;
312 }
313
314 private async startChargingStation(
315 index: number,
316 stationTemplateUrl: StationTemplateUrl
317 ): Promise<void> {
318 await this.workerImplementation?.addElement({
319 index,
320 templateFile: path.join(
321 path.dirname(fileURLToPath(import.meta.url)),
322 'assets',
323 'station-templates',
324 stationTemplateUrl.file
325 ),
326 });
327 }
328
329 private gracefulShutdown = (): void => {
330 console.info(`${chalk.green('Graceful shutdown')}`);
331 this.stop()
332 .then(() => {
333 process.exit(0);
334 })
335 .catch((error) => {
336 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error);
337 process.exit(1);
338 });
339 };
340
341 private waitForChargingStationsStopped = async (
342 stoppedEventsToWait = this.numberOfStartedChargingStations
343 ): Promise<number> => {
344 return new Promise((resolve) => {
345 let stoppedEvents = 0;
346 if (stoppedEventsToWait === 0) {
347 resolve(stoppedEvents);
348 }
349 this.on(ChargingStationWorkerMessageEvents.stopped, () => {
350 ++stoppedEvents;
351 if (stoppedEvents === stoppedEventsToWait) {
352 resolve(stoppedEvents);
353 }
354 });
355 });
356 };
357
358 private logPrefix = (): string => {
359 return Utils.logPrefix(' Bootstrap |');
360 };
361 }