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