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