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