refactor: use a directory to store performance related files
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
1 import fs from 'node:fs';
2 import path from 'node:path';
3 import { fileURLToPath } from 'node:url';
4
5 import chalk from 'chalk';
6 import merge from 'just-merge';
7 import { WorkerChoiceStrategies } from 'poolifier';
8
9 import { Constants } from './Constants';
10 import { Utils } from './Utils';
11 import {
12 ApplicationProtocol,
13 type ConfigurationData,
14 FileType,
15 type LogConfiguration,
16 type StationTemplateUrl,
17 type StorageConfiguration,
18 StorageType,
19 SupervisionUrlDistribution,
20 type UIServerConfiguration,
21 type WorkerConfiguration,
22 } from '../types';
23 import { WorkerConstants, WorkerProcessType } from '../worker';
24
25 export class Configuration {
26 private static configurationFile = path.join(
27 path.dirname(fileURLToPath(import.meta.url)),
28 'assets',
29 'config.json'
30 );
31
32 private static configurationFileWatcher: fs.FSWatcher | undefined;
33 private static configuration: ConfigurationData | null = null;
34 private static configurationChangeCallback: () => Promise<void>;
35
36 private constructor() {
37 // This is intentional
38 }
39
40 public static setConfigurationChangeCallback(cb: () => Promise<void>): void {
41 Configuration.configurationChangeCallback = cb;
42 }
43
44 public static getUIServer(): UIServerConfiguration {
45 if (Utils.hasOwnProp(Configuration.getConfig(), 'uiWebSocketServer')) {
46 console.error(
47 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
48 "Deprecated configuration section 'uiWebSocketServer' usage. Use 'uiServer' instead"
49 )}`
50 );
51 }
52 let uiServerConfiguration: UIServerConfiguration = {
53 enabled: false,
54 type: ApplicationProtocol.WS,
55 options: {
56 host: Constants.DEFAULT_UI_SERVER_HOST,
57 port: Constants.DEFAULT_UI_SERVER_PORT,
58 },
59 };
60 if (Utils.hasOwnProp(Configuration.getConfig(), 'uiServer')) {
61 uiServerConfiguration = merge<UIServerConfiguration>(
62 uiServerConfiguration,
63 Configuration.getConfig()?.uiServer
64 );
65 }
66 if (Utils.isCFEnvironment() === true) {
67 delete uiServerConfiguration.options?.host;
68 uiServerConfiguration.options.port = parseInt(process.env.PORT);
69 }
70 return uiServerConfiguration;
71 }
72
73 public static getPerformanceStorage(): StorageConfiguration {
74 Configuration.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
75 let storageConfiguration: StorageConfiguration = {
76 enabled: false,
77 type: StorageType.JSON_FILE,
78 uri: this.getDefaultPerformanceStorageUri(StorageType.JSON_FILE),
79 };
80 if (Utils.hasOwnProp(Configuration.getConfig(), 'performanceStorage')) {
81 storageConfiguration = {
82 ...storageConfiguration,
83 ...Configuration.getConfig()?.performanceStorage,
84 ...(Configuration.getConfig()?.performanceStorage?.type === StorageType.JSON_FILE &&
85 Configuration.getConfig()?.performanceStorage?.uri && {
86 uri: Configuration.buildPerformanceUriFilePath(
87 new URL(Configuration.getConfig()?.performanceStorage?.uri).pathname
88 ),
89 }),
90 };
91 }
92 return storageConfiguration;
93 }
94
95 public static getAutoReconnectMaxRetries(): number | undefined {
96 Configuration.warnDeprecatedConfigurationKey(
97 'autoReconnectTimeout',
98 undefined,
99 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
100 );
101 Configuration.warnDeprecatedConfigurationKey(
102 'connectionTimeout',
103 undefined,
104 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
105 );
106 Configuration.warnDeprecatedConfigurationKey(
107 'autoReconnectMaxRetries',
108 undefined,
109 'Use it in charging station template instead'
110 );
111 // Read conf
112 if (Utils.hasOwnProp(Configuration.getConfig(), 'autoReconnectMaxRetries')) {
113 return Configuration.getConfig()?.autoReconnectMaxRetries;
114 }
115 }
116
117 public static getStationTemplateUrls(): StationTemplateUrl[] | undefined {
118 Configuration.warnDeprecatedConfigurationKey(
119 'stationTemplateURLs',
120 undefined,
121 "Use 'stationTemplateUrls' instead"
122 );
123 !Utils.isUndefined(Configuration.getConfig()['stationTemplateURLs']) &&
124 (Configuration.getConfig().stationTemplateUrls = Configuration.getConfig()[
125 'stationTemplateURLs'
126 ] as StationTemplateUrl[]);
127 Configuration.getConfig().stationTemplateUrls.forEach(
128 (stationTemplateUrl: StationTemplateUrl) => {
129 if (!Utils.isUndefined(stationTemplateUrl['numberOfStation'])) {
130 console.error(
131 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
132 `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`
133 )}`
134 );
135 }
136 }
137 );
138 // Read conf
139 return Configuration.getConfig()?.stationTemplateUrls;
140 }
141
142 public static getLog(): LogConfiguration {
143 Configuration.warnDeprecatedConfigurationKey(
144 'logEnabled',
145 undefined,
146 "Use 'log' section to define the logging enablement instead"
147 );
148 Configuration.warnDeprecatedConfigurationKey(
149 'logFile',
150 undefined,
151 "Use 'log' section to define the log file instead"
152 );
153 Configuration.warnDeprecatedConfigurationKey(
154 'logErrorFile',
155 undefined,
156 "Use 'log' section to define the log error file instead"
157 );
158 Configuration.warnDeprecatedConfigurationKey(
159 'logConsole',
160 undefined,
161 "Use 'log' section to define the console logging enablement instead"
162 );
163 Configuration.warnDeprecatedConfigurationKey(
164 'logStatisticsInterval',
165 undefined,
166 "Use 'log' section to define the log statistics interval instead"
167 );
168 Configuration.warnDeprecatedConfigurationKey(
169 'logLevel',
170 undefined,
171 "Use 'log' section to define the log level instead"
172 );
173 Configuration.warnDeprecatedConfigurationKey(
174 'logFormat',
175 undefined,
176 "Use 'log' section to define the log format instead"
177 );
178 Configuration.warnDeprecatedConfigurationKey(
179 'logRotate',
180 undefined,
181 "Use 'log' section to define the log rotation enablement instead"
182 );
183 Configuration.warnDeprecatedConfigurationKey(
184 'logMaxFiles',
185 undefined,
186 "Use 'log' section to define the log maximum files instead"
187 );
188 Configuration.warnDeprecatedConfigurationKey(
189 'logMaxSize',
190 undefined,
191 "Use 'log' section to define the log maximum size instead"
192 );
193 const defaultLogConfiguration: LogConfiguration = {
194 enabled: true,
195 file: 'logs/combined.log',
196 errorFile: 'logs/error.log',
197 statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
198 level: 'info',
199 format: 'simple',
200 rotate: true,
201 };
202 const deprecatedLogConfiguration: LogConfiguration = {
203 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logEnabled') && {
204 enabled: Configuration.getConfig()?.logEnabled,
205 }),
206 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logFile') && {
207 file: Configuration.getConfig()?.logFile,
208 }),
209 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logErrorFile') && {
210 errorFile: Configuration.getConfig()?.logErrorFile,
211 }),
212 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logStatisticsInterval') && {
213 statisticsInterval: Configuration.getConfig()?.logStatisticsInterval,
214 }),
215 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logLevel') && {
216 level: Configuration.getConfig()?.logLevel,
217 }),
218 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logConsole') && {
219 console: Configuration.getConfig()?.logConsole,
220 }),
221 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logFormat') && {
222 format: Configuration.getConfig()?.logFormat,
223 }),
224 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logRotate') && {
225 rotate: Configuration.getConfig()?.logRotate,
226 }),
227 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logMaxFiles') && {
228 maxFiles: Configuration.getConfig()?.logMaxFiles,
229 }),
230 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logMaxSize') && {
231 maxSize: Configuration.getConfig()?.logMaxSize,
232 }),
233 };
234 const logConfiguration: LogConfiguration = {
235 ...defaultLogConfiguration,
236 ...deprecatedLogConfiguration,
237 ...(Utils.hasOwnProp(Configuration.getConfig(), 'log') && Configuration.getConfig()?.log),
238 };
239 return logConfiguration;
240 }
241
242 public static getWorker(): WorkerConfiguration {
243 Configuration.warnDeprecatedConfigurationKey(
244 'useWorkerPool',
245 undefined,
246 "Use 'worker' section to define the type of worker process model instead"
247 );
248 Configuration.warnDeprecatedConfigurationKey(
249 'workerProcess',
250 undefined,
251 "Use 'worker' section to define the type of worker process model instead"
252 );
253 Configuration.warnDeprecatedConfigurationKey(
254 'workerStartDelay',
255 undefined,
256 "Use 'worker' section to define the worker start delay instead"
257 );
258 Configuration.warnDeprecatedConfigurationKey(
259 'chargingStationsPerWorker',
260 undefined,
261 "Use 'worker' section to define the number of element(s) per worker instead"
262 );
263 Configuration.warnDeprecatedConfigurationKey(
264 'elementStartDelay',
265 undefined,
266 "Use 'worker' section to define the worker's element start delay instead"
267 );
268 Configuration.warnDeprecatedConfigurationKey(
269 'workerPoolMinSize',
270 undefined,
271 "Use 'worker' section to define the worker pool minimum size instead"
272 );
273 Configuration.warnDeprecatedConfigurationKey(
274 'workerPoolSize;',
275 undefined,
276 "Use 'worker' section to define the worker pool maximum size instead"
277 );
278 Configuration.warnDeprecatedConfigurationKey(
279 'workerPoolMaxSize;',
280 undefined,
281 "Use 'worker' section to define the worker pool maximum size instead"
282 );
283 Configuration.warnDeprecatedConfigurationKey(
284 'workerPoolStrategy;',
285 undefined,
286 "Use 'worker' section to define the worker pool strategy instead"
287 );
288 const defaultWorkerConfiguration: WorkerConfiguration = {
289 processType: WorkerProcessType.workerSet,
290 startDelay: WorkerConstants.DEFAULT_WORKER_START_DELAY,
291 elementsPerWorker: WorkerConstants.DEFAULT_ELEMENTS_PER_WORKER,
292 elementStartDelay: WorkerConstants.DEFAULT_ELEMENT_START_DELAY,
293 poolMinSize: WorkerConstants.DEFAULT_POOL_MIN_SIZE,
294 poolMaxSize: WorkerConstants.DEFAULT_POOL_MAX_SIZE,
295 poolStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
296 };
297 const deprecatedWorkerConfiguration: WorkerConfiguration = {
298 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerProcess') && {
299 processType: Configuration.getConfig()?.workerProcess,
300 }),
301 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerStartDelay') && {
302 startDelay: Configuration.getConfig()?.workerStartDelay,
303 }),
304 ...(Utils.hasOwnProp(Configuration.getConfig(), 'chargingStationsPerWorker') && {
305 elementsPerWorker: Configuration.getConfig()?.chargingStationsPerWorker,
306 }),
307 ...(Utils.hasOwnProp(Configuration.getConfig(), 'elementStartDelay') && {
308 elementStartDelay: Configuration.getConfig()?.elementStartDelay,
309 }),
310 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolMinSize') && {
311 poolMinSize: Configuration.getConfig()?.workerPoolMinSize,
312 }),
313 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolMaxSize') && {
314 poolMaxSize: Configuration.getConfig()?.workerPoolMaxSize,
315 }),
316 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolStrategy') && {
317 poolStrategy:
318 Configuration.getConfig()?.workerPoolStrategy ?? WorkerChoiceStrategies.ROUND_ROBIN,
319 }),
320 };
321 const workerConfiguration: WorkerConfiguration = {
322 ...defaultWorkerConfiguration,
323 ...deprecatedWorkerConfiguration,
324 ...(Utils.hasOwnProp(Configuration.getConfig(), 'worker') &&
325 Configuration.getConfig()?.worker),
326 };
327 return workerConfiguration;
328 }
329
330 public static workerPoolInUse(): boolean {
331 return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
332 Configuration.getWorker().processType
333 );
334 }
335
336 public static workerDynamicPoolInUse(): boolean {
337 return Configuration.getWorker().processType === WorkerProcessType.dynamicPool;
338 }
339
340 public static getSupervisionUrls(): string | string[] | undefined {
341 Configuration.warnDeprecatedConfigurationKey(
342 'supervisionURLs',
343 undefined,
344 "Use 'supervisionUrls' instead"
345 );
346 !Utils.isUndefined(Configuration.getConfig()['supervisionURLs']) &&
347 (Configuration.getConfig().supervisionUrls = Configuration.getConfig()['supervisionURLs'] as
348 | string
349 | string[]);
350 // Read conf
351 return Configuration.getConfig()?.supervisionUrls;
352 }
353
354 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution | undefined {
355 Configuration.warnDeprecatedConfigurationKey(
356 'distributeStationToTenantEqually',
357 undefined,
358 "Use 'supervisionUrlDistribution' instead"
359 );
360 Configuration.warnDeprecatedConfigurationKey(
361 'distributeStationsToTenantsEqually',
362 undefined,
363 "Use 'supervisionUrlDistribution' instead"
364 );
365 return Utils.hasOwnProp(Configuration.getConfig(), 'supervisionUrlDistribution')
366 ? Configuration.getConfig()?.supervisionUrlDistribution
367 : SupervisionUrlDistribution.ROUND_ROBIN;
368 }
369
370 private static logPrefix = (): string => {
371 return `${new Date().toLocaleString()} Simulator configuration |`;
372 };
373
374 private static warnDeprecatedConfigurationKey(
375 key: string,
376 sectionName?: string,
377 logMsgToAppend = ''
378 ) {
379 if (
380 sectionName &&
381 !Utils.isUndefined(Configuration.getConfig()[sectionName]) &&
382 !Utils.isUndefined((Configuration.getConfig()[sectionName] as Record<string, unknown>)[key])
383 ) {
384 console.error(
385 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
386 `Deprecated configuration key '${key}' usage in section '${sectionName}'${
387 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
388 }`
389 )}`
390 );
391 } else if (!Utils.isUndefined(Configuration.getConfig()[key])) {
392 console.error(
393 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
394 `Deprecated configuration key '${key}' usage${
395 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
396 }`
397 )}`
398 );
399 }
400 }
401
402 // Read the config file
403 private static getConfig(): ConfigurationData | null {
404 if (!Configuration.configuration) {
405 try {
406 Configuration.configuration = JSON.parse(
407 fs.readFileSync(Configuration.configurationFile, 'utf8')
408 ) as ConfigurationData;
409 } catch (error) {
410 Configuration.handleFileException(
411 Configuration.configurationFile,
412 FileType.Configuration,
413 error as NodeJS.ErrnoException,
414 Configuration.logPrefix()
415 );
416 }
417 if (!Configuration.configurationFileWatcher) {
418 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
419 }
420 }
421 return Configuration.configuration;
422 }
423
424 private static getConfigurationFileWatcher(): fs.FSWatcher | undefined {
425 try {
426 return fs.watch(Configuration.configurationFile, (event, filename): void => {
427 if (filename?.trim().length > 0 && event === 'change') {
428 // Nullify to force configuration file reading
429 Configuration.configuration = null;
430 if (!Utils.isUndefined(Configuration.configurationChangeCallback)) {
431 Configuration.configurationChangeCallback().catch((error) => {
432 throw typeof error === 'string' ? new Error(error) : error;
433 });
434 }
435 }
436 });
437 } catch (error) {
438 Configuration.handleFileException(
439 Configuration.configurationFile,
440 FileType.Configuration,
441 error as NodeJS.ErrnoException,
442 Configuration.logPrefix()
443 );
444 }
445 }
446
447 private static handleFileException(
448 file: string,
449 fileType: FileType,
450 error: NodeJS.ErrnoException,
451 logPrefix: string
452 ): void {
453 const prefix = Utils.isNotEmptyString(logPrefix) ? `${logPrefix} ` : '';
454 let logMsg: string;
455 switch (error.code) {
456 case 'ENOENT':
457 logMsg = `${fileType} file ${file} not found:`;
458 break;
459 case 'EEXIST':
460 logMsg = `${fileType} file ${file} already exists:`;
461 break;
462 case 'EACCES':
463 logMsg = `${fileType} file ${file} access denied:`;
464 break;
465 case 'EPERM':
466 logMsg = `${fileType} file ${file} permission denied:`;
467 break;
468 default:
469 logMsg = `${fileType} file ${file} error:`;
470 }
471 console.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
472 throw error;
473 }
474
475 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
476 switch (storageType) {
477 case StorageType.JSON_FILE:
478 return Configuration.buildPerformanceUriFilePath(
479 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`
480 );
481 case StorageType.SQLITE:
482 return Configuration.buildPerformanceUriFilePath(
483 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`
484 );
485 default:
486 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
487 }
488 }
489
490 private static buildPerformanceUriFilePath(file: string) {
491 return `file://${path.join(
492 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
493 file
494 )}`;
495 }
496 }