e1fd11c731af1024ebfd08dfba373eb82680abc2
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
1 import { type FSWatcher, readFileSync, watch } from 'node:fs'
2 import { dirname, join } from 'node:path'
3 import { env } from 'node:process'
4 import { fileURLToPath } from 'node:url'
5
6 import chalk from 'chalk'
7 import { mergeDeepRight, once } from 'rambda'
8
9 import {
10 ApplicationProtocol,
11 ApplicationProtocolVersion,
12 type ConfigurationData,
13 ConfigurationSection,
14 FileType,
15 type LogConfiguration,
16 type StationTemplateUrl,
17 type StorageConfiguration,
18 StorageType,
19 SupervisionUrlDistribution,
20 type UIServerConfiguration,
21 type WorkerConfiguration
22 } from '../types/index.js'
23 import {
24 DEFAULT_ELEMENT_ADD_DELAY,
25 DEFAULT_POOL_MAX_SIZE,
26 DEFAULT_POOL_MIN_SIZE,
27 DEFAULT_WORKER_START_DELAY,
28 WorkerProcessType
29 } from '../worker/index.js'
30 import {
31 buildPerformanceUriFilePath,
32 checkWorkerElementsPerWorker,
33 checkWorkerProcessType,
34 getDefaultPerformanceStorageUri,
35 handleFileException,
36 logPrefix
37 } from './ConfigurationUtils.js'
38 import { Constants } from './Constants.js'
39 import { hasOwnProp, isCFEnvironment } from './Utils.js'
40
41 type ConfigurationSectionType =
42 | LogConfiguration
43 | StorageConfiguration
44 | WorkerConfiguration
45 | UIServerConfiguration
46
47 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
48 export class Configuration {
49 public static configurationChangeCallback?: () => Promise<void>
50
51 private static readonly configurationFile = join(
52 dirname(fileURLToPath(import.meta.url)),
53 'assets',
54 'config.json'
55 )
56
57 private static configurationFileReloading = false
58 private static configurationData?: ConfigurationData
59 private static configurationFileWatcher?: FSWatcher
60 private static readonly configurationSectionCache = new Map<
61 ConfigurationSection,
62 ConfigurationSectionType
63 >([
64 [ConfigurationSection.log, Configuration.buildLogSection()],
65 [ConfigurationSection.performanceStorage, Configuration.buildPerformanceStorageSection()],
66 [ConfigurationSection.worker, Configuration.buildWorkerSection()],
67 [ConfigurationSection.uiServer, Configuration.buildUIServerSection()]
68 ])
69
70 private constructor () {
71 // This is intentional
72 }
73
74 public static getConfigurationSection<T extends ConfigurationSectionType>(
75 sectionName: ConfigurationSection
76 ): T {
77 if (!Configuration.isConfigurationSectionCached(sectionName)) {
78 Configuration.cacheConfigurationSection(sectionName)
79 }
80 return Configuration.configurationSectionCache.get(sectionName) as T
81 }
82
83 public static getStationTemplateUrls (): StationTemplateUrl[] | undefined {
84 const checkDeprecatedConfigurationKeysOnce = once(
85 Configuration.checkDeprecatedConfigurationKeys.bind(Configuration)
86 )
87 checkDeprecatedConfigurationKeysOnce()
88 return Configuration.getConfigurationData()?.stationTemplateUrls
89 }
90
91 public static getSupervisionUrls (): string | string[] | undefined {
92 if (
93 Configuration.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData] != null
94 ) {
95 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
96 Configuration.getConfigurationData()!.supervisionUrls = Configuration.getConfigurationData()![
97 'supervisionURLs' as keyof ConfigurationData
98 ] as string | string[]
99 }
100 return Configuration.getConfigurationData()?.supervisionUrls
101 }
102
103 public static getSupervisionUrlDistribution (): SupervisionUrlDistribution | undefined {
104 return hasOwnProp(Configuration.getConfigurationData(), 'supervisionUrlDistribution')
105 ? Configuration.getConfigurationData()?.supervisionUrlDistribution
106 : SupervisionUrlDistribution.ROUND_ROBIN
107 }
108
109 public static workerPoolInUse (): boolean {
110 return [WorkerProcessType.dynamicPool, WorkerProcessType.fixedPool].includes(
111 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
112 Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
113 .processType!
114 )
115 }
116
117 public static workerDynamicPoolInUse (): boolean {
118 return (
119 Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
120 .processType === WorkerProcessType.dynamicPool
121 )
122 }
123
124 private static isConfigurationSectionCached (sectionName: ConfigurationSection): boolean {
125 return Configuration.configurationSectionCache.has(sectionName)
126 }
127
128 private static cacheConfigurationSection (sectionName: ConfigurationSection): void {
129 switch (sectionName) {
130 case ConfigurationSection.log:
131 Configuration.configurationSectionCache.set(sectionName, Configuration.buildLogSection())
132 break
133 case ConfigurationSection.performanceStorage:
134 Configuration.configurationSectionCache.set(
135 sectionName,
136 Configuration.buildPerformanceStorageSection()
137 )
138 break
139 case ConfigurationSection.worker:
140 Configuration.configurationSectionCache.set(sectionName, Configuration.buildWorkerSection())
141 break
142 case ConfigurationSection.uiServer:
143 Configuration.configurationSectionCache.set(
144 sectionName,
145 Configuration.buildUIServerSection()
146 )
147 break
148 default:
149 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
150 throw new Error(`Unknown configuration section '${sectionName}'`)
151 }
152 }
153
154 private static buildUIServerSection (): UIServerConfiguration {
155 let uiServerConfiguration: UIServerConfiguration = {
156 enabled: false,
157 type: ApplicationProtocol.WS,
158 version: ApplicationProtocolVersion.VERSION_11,
159 options: {
160 host: Constants.DEFAULT_UI_SERVER_HOST,
161 port: Constants.DEFAULT_UI_SERVER_PORT
162 }
163 }
164 if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.uiServer)) {
165 uiServerConfiguration = mergeDeepRight(
166 uiServerConfiguration,
167 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
168 Configuration.getConfigurationData()!.uiServer!
169 )
170 }
171 if (isCFEnvironment()) {
172 delete uiServerConfiguration.options?.host
173 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
174 uiServerConfiguration.options!.port = parseInt(env.PORT!)
175 }
176 return uiServerConfiguration
177 }
178
179 private static buildPerformanceStorageSection (): StorageConfiguration {
180 let storageConfiguration: StorageConfiguration
181 switch (Configuration.getConfigurationData()?.performanceStorage?.type) {
182 case StorageType.SQLITE:
183 storageConfiguration = {
184 enabled: false,
185 type: StorageType.SQLITE,
186 uri: getDefaultPerformanceStorageUri(StorageType.SQLITE)
187 }
188 break
189 case StorageType.JSON_FILE:
190 storageConfiguration = {
191 enabled: false,
192 type: StorageType.JSON_FILE,
193 uri: getDefaultPerformanceStorageUri(StorageType.JSON_FILE)
194 }
195 break
196 case StorageType.NONE:
197 default:
198 storageConfiguration = {
199 enabled: true,
200 type: StorageType.NONE
201 }
202 break
203 }
204 if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.performanceStorage)) {
205 storageConfiguration = {
206 ...storageConfiguration,
207 ...Configuration.getConfigurationData()?.performanceStorage,
208 ...((Configuration.getConfigurationData()?.performanceStorage?.type ===
209 StorageType.JSON_FILE ||
210 Configuration.getConfigurationData()?.performanceStorage?.type === StorageType.SQLITE) &&
211 Configuration.getConfigurationData()?.performanceStorage?.uri != null && {
212 uri: buildPerformanceUriFilePath(
213 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
214 new URL(Configuration.getConfigurationData()!.performanceStorage!.uri!).pathname
215 )
216 })
217 }
218 }
219 return storageConfiguration
220 }
221
222 private static buildLogSection (): LogConfiguration {
223 const defaultLogConfiguration: LogConfiguration = {
224 enabled: true,
225 file: 'logs/combined.log',
226 errorFile: 'logs/error.log',
227 statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
228 level: 'info',
229 format: 'simple',
230 rotate: true
231 }
232 const deprecatedLogConfiguration: LogConfiguration = {
233 ...(hasOwnProp(Configuration.getConfigurationData(), 'logEnabled') && {
234 enabled: Configuration.getConfigurationData()?.logEnabled
235 }),
236 ...(hasOwnProp(Configuration.getConfigurationData(), 'logFile') && {
237 file: Configuration.getConfigurationData()?.logFile
238 }),
239 ...(hasOwnProp(Configuration.getConfigurationData(), 'logErrorFile') && {
240 errorFile: Configuration.getConfigurationData()?.logErrorFile
241 }),
242 ...(hasOwnProp(Configuration.getConfigurationData(), 'logStatisticsInterval') && {
243 statisticsInterval: Configuration.getConfigurationData()?.logStatisticsInterval
244 }),
245 ...(hasOwnProp(Configuration.getConfigurationData(), 'logLevel') && {
246 level: Configuration.getConfigurationData()?.logLevel
247 }),
248 ...(hasOwnProp(Configuration.getConfigurationData(), 'logConsole') && {
249 console: Configuration.getConfigurationData()?.logConsole
250 }),
251 ...(hasOwnProp(Configuration.getConfigurationData(), 'logFormat') && {
252 format: Configuration.getConfigurationData()?.logFormat
253 }),
254 ...(hasOwnProp(Configuration.getConfigurationData(), 'logRotate') && {
255 rotate: Configuration.getConfigurationData()?.logRotate
256 }),
257 ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxFiles') && {
258 maxFiles: Configuration.getConfigurationData()?.logMaxFiles
259 }),
260 ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxSize') && {
261 maxSize: Configuration.getConfigurationData()?.logMaxSize
262 })
263 }
264 const logConfiguration: LogConfiguration = {
265 ...defaultLogConfiguration,
266 ...deprecatedLogConfiguration,
267 ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.log) &&
268 Configuration.getConfigurationData()?.log)
269 }
270 return logConfiguration
271 }
272
273 private static buildWorkerSection (): WorkerConfiguration {
274 const defaultWorkerConfiguration: WorkerConfiguration = {
275 processType: WorkerProcessType.workerSet,
276 startDelay: DEFAULT_WORKER_START_DELAY,
277 elementsPerWorker: 'auto',
278 elementAddDelay: DEFAULT_ELEMENT_ADD_DELAY,
279 poolMinSize: DEFAULT_POOL_MIN_SIZE,
280 poolMaxSize: DEFAULT_POOL_MAX_SIZE
281 }
282
283 const deprecatedWorkerConfiguration: WorkerConfiguration = {
284 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerProcess') && {
285 processType: Configuration.getConfigurationData()?.workerProcess
286 }),
287 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerStartDelay') && {
288 startDelay: Configuration.getConfigurationData()?.workerStartDelay
289 }),
290 ...(hasOwnProp(Configuration.getConfigurationData(), 'chargingStationsPerWorker') && {
291 elementsPerWorker: Configuration.getConfigurationData()?.chargingStationsPerWorker
292 }),
293 ...(hasOwnProp(Configuration.getConfigurationData(), 'elementAddDelay') && {
294 elementAddDelay: Configuration.getConfigurationData()?.elementAddDelay
295 }),
296 ...(hasOwnProp(Configuration.getConfigurationData()?.worker, 'elementStartDelay') && {
297 elementAddDelay: Configuration.getConfigurationData()?.worker?.elementStartDelay
298 }),
299 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMinSize') && {
300 poolMinSize: Configuration.getConfigurationData()?.workerPoolMinSize
301 }),
302 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMaxSize') && {
303 poolMaxSize: Configuration.getConfigurationData()?.workerPoolMaxSize
304 })
305 }
306 hasOwnProp(Configuration.getConfigurationData(), 'workerPoolStrategy') &&
307 delete Configuration.getConfigurationData()?.workerPoolStrategy
308 const workerConfiguration: WorkerConfiguration = {
309 ...defaultWorkerConfiguration,
310 ...deprecatedWorkerConfiguration,
311 ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.worker) &&
312 Configuration.getConfigurationData()?.worker)
313 }
314 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
315 checkWorkerProcessType(workerConfiguration.processType!)
316 checkWorkerElementsPerWorker(workerConfiguration.elementsPerWorker)
317 return workerConfiguration
318 }
319
320 private static checkDeprecatedConfigurationKeys (): void {
321 // connection timeout
322 Configuration.warnDeprecatedConfigurationKey(
323 'autoReconnectTimeout',
324 undefined,
325 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
326 )
327 Configuration.warnDeprecatedConfigurationKey(
328 'connectionTimeout',
329 undefined,
330 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
331 )
332 // connection retries
333 Configuration.warnDeprecatedConfigurationKey(
334 'autoReconnectMaxRetries',
335 undefined,
336 'Use it in charging station template instead'
337 )
338 // station template url(s)
339 Configuration.warnDeprecatedConfigurationKey(
340 'stationTemplateURLs',
341 undefined,
342 "Use 'stationTemplateUrls' instead"
343 )
344 Configuration.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData] !=
345 null &&
346 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
347 (Configuration.getConfigurationData()!.stationTemplateUrls =
348 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
349 Configuration.getConfigurationData()![
350 'stationTemplateURLs' as keyof ConfigurationData
351 ] as StationTemplateUrl[])
352 Configuration.getConfigurationData()?.stationTemplateUrls.forEach(
353 (stationTemplateUrl: StationTemplateUrl) => {
354 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
355 if (stationTemplateUrl['numberOfStation' as keyof StationTemplateUrl] != null) {
356 console.error(
357 `${chalk.green(logPrefix())} ${chalk.red(
358 `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`
359 )}`
360 )
361 }
362 }
363 )
364 // supervision url(s)
365 Configuration.warnDeprecatedConfigurationKey(
366 'supervisionURLs',
367 undefined,
368 "Use 'supervisionUrls' instead"
369 )
370 // supervision urls distribution
371 Configuration.warnDeprecatedConfigurationKey(
372 'distributeStationToTenantEqually',
373 undefined,
374 "Use 'supervisionUrlDistribution' instead"
375 )
376 Configuration.warnDeprecatedConfigurationKey(
377 'distributeStationsToTenantsEqually',
378 undefined,
379 "Use 'supervisionUrlDistribution' instead"
380 )
381 // worker section
382 Configuration.warnDeprecatedConfigurationKey(
383 'useWorkerPool',
384 undefined,
385 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
386 )
387 Configuration.warnDeprecatedConfigurationKey(
388 'workerProcess',
389 undefined,
390 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
391 )
392 Configuration.warnDeprecatedConfigurationKey(
393 'workerStartDelay',
394 undefined,
395 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`
396 )
397 Configuration.warnDeprecatedConfigurationKey(
398 'chargingStationsPerWorker',
399 undefined,
400 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`
401 )
402 Configuration.warnDeprecatedConfigurationKey(
403 'elementAddDelay',
404 undefined,
405 `Use '${ConfigurationSection.worker}' section to define the worker's element add delay instead`
406 )
407 Configuration.warnDeprecatedConfigurationKey(
408 'workerPoolMinSize',
409 undefined,
410 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`
411 )
412 Configuration.warnDeprecatedConfigurationKey(
413 'workerPoolSize',
414 undefined,
415 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
416 )
417 Configuration.warnDeprecatedConfigurationKey(
418 'workerPoolMaxSize',
419 undefined,
420 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
421 )
422 Configuration.warnDeprecatedConfigurationKey(
423 'workerPoolStrategy',
424 undefined,
425 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`
426 )
427 Configuration.warnDeprecatedConfigurationKey(
428 'poolStrategy',
429 ConfigurationSection.worker,
430 'Not publicly exposed to end users'
431 )
432 Configuration.warnDeprecatedConfigurationKey(
433 'elementStartDelay',
434 ConfigurationSection.worker,
435 "Use 'elementAddDelay' instead"
436 )
437 if (
438 Configuration.getConfigurationData()?.worker?.processType ===
439 ('staticPool' as WorkerProcessType)
440 ) {
441 console.error(
442 `${chalk.green(logPrefix())} ${chalk.red(
443 `Deprecated configuration 'staticPool' value usage in worker section 'processType' field. Use '${WorkerProcessType.fixedPool}' value instead`
444 )}`
445 )
446 }
447 // log section
448 Configuration.warnDeprecatedConfigurationKey(
449 'logEnabled',
450 undefined,
451 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`
452 )
453 Configuration.warnDeprecatedConfigurationKey(
454 'logFile',
455 undefined,
456 `Use '${ConfigurationSection.log}' section to define the log file instead`
457 )
458 Configuration.warnDeprecatedConfigurationKey(
459 'logErrorFile',
460 undefined,
461 `Use '${ConfigurationSection.log}' section to define the log error file instead`
462 )
463 Configuration.warnDeprecatedConfigurationKey(
464 'logConsole',
465 undefined,
466 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`
467 )
468 Configuration.warnDeprecatedConfigurationKey(
469 'logStatisticsInterval',
470 undefined,
471 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`
472 )
473 Configuration.warnDeprecatedConfigurationKey(
474 'logLevel',
475 undefined,
476 `Use '${ConfigurationSection.log}' section to define the log level instead`
477 )
478 Configuration.warnDeprecatedConfigurationKey(
479 'logFormat',
480 undefined,
481 `Use '${ConfigurationSection.log}' section to define the log format instead`
482 )
483 Configuration.warnDeprecatedConfigurationKey(
484 'logRotate',
485 undefined,
486 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`
487 )
488 Configuration.warnDeprecatedConfigurationKey(
489 'logMaxFiles',
490 undefined,
491 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`
492 )
493 Configuration.warnDeprecatedConfigurationKey(
494 'logMaxSize',
495 undefined,
496 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`
497 )
498 // performanceStorage section
499 Configuration.warnDeprecatedConfigurationKey(
500 'URI',
501 ConfigurationSection.performanceStorage,
502 "Use 'uri' instead"
503 )
504 // uiServer section
505 if (hasOwnProp(Configuration.getConfigurationData(), 'uiWebSocketServer')) {
506 console.error(
507 `${chalk.green(logPrefix())} ${chalk.red(
508 `Deprecated configuration section 'uiWebSocketServer' usage. Use '${ConfigurationSection.uiServer}' instead`
509 )}`
510 )
511 }
512 }
513
514 private static warnDeprecatedConfigurationKey (
515 key: string,
516 sectionName?: string,
517 logMsgToAppend = ''
518 ): void {
519 if (
520 sectionName != null &&
521 Configuration.getConfigurationData()?.[sectionName as keyof ConfigurationData] != null &&
522 (
523 Configuration.getConfigurationData()?.[sectionName as keyof ConfigurationData] as Record<
524 string,
525 unknown
526 >
527 )[key] != null
528 ) {
529 console.error(
530 `${chalk.green(logPrefix())} ${chalk.red(
531 `Deprecated configuration key '${key}' usage in section '${sectionName}'${
532 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
533 }`
534 )}`
535 )
536 } else if (Configuration.getConfigurationData()?.[key as keyof ConfigurationData] != null) {
537 console.error(
538 `${chalk.green(logPrefix())} ${chalk.red(
539 `Deprecated configuration key '${key}' usage${
540 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
541 }`
542 )}`
543 )
544 }
545 }
546
547 private static getConfigurationData (): ConfigurationData | undefined {
548 if (Configuration.configurationData == null) {
549 try {
550 Configuration.configurationData = JSON.parse(
551 readFileSync(Configuration.configurationFile, 'utf8')
552 ) as ConfigurationData
553 if (Configuration.configurationFileWatcher == null) {
554 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher()
555 }
556 } catch (error) {
557 handleFileException(
558 Configuration.configurationFile,
559 FileType.Configuration,
560 error as NodeJS.ErrnoException,
561 logPrefix()
562 )
563 }
564 }
565 return Configuration.configurationData
566 }
567
568 private static getConfigurationFileWatcher (): FSWatcher | undefined {
569 try {
570 return watch(Configuration.configurationFile, (event, filename): void => {
571 if (
572 !Configuration.configurationFileReloading &&
573 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
574 filename!.trim().length > 0 &&
575 event === 'change'
576 ) {
577 Configuration.configurationFileReloading = true
578 const consoleWarnOnce = once(console.warn)
579 consoleWarnOnce(
580 `${chalk.green(logPrefix())} ${chalk.yellow(
581 `${FileType.Configuration} ${this.configurationFile} file have changed, reload`
582 )}`
583 )
584 delete Configuration.configurationData
585 Configuration.configurationSectionCache.clear()
586 if (Configuration.configurationChangeCallback != null) {
587 Configuration.configurationChangeCallback()
588 .catch((error: unknown) => {
589 throw typeof error === 'string' ? new Error(error) : error
590 })
591 .finally(() => {
592 Configuration.configurationFileReloading = false
593 })
594 } else {
595 Configuration.configurationFileReloading = false
596 }
597 }
598 })
599 } catch (error) {
600 handleFileException(
601 Configuration.configurationFile,
602 FileType.Configuration,
603 error as NodeJS.ErrnoException,
604 logPrefix()
605 )
606 }
607 }
608 }