From 4354af5acc068587187dbe3729a5ca8d54fb9626 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 29 Nov 2023 10:12:14 +0100 Subject: [PATCH] refactor: factor out configuration handling helpers MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- src/charging-station/Bootstrap.ts | 15 ++--- src/utils/Configuration.ts | 100 +++++++----------------------- src/utils/ConfigurationUtils.ts | 88 ++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 84 deletions(-) create mode 100644 src/utils/ConfigurationUtils.ts diff --git a/src/charging-station/Bootstrap.ts b/src/charging-station/Bootstrap.ts index 71c994b5..66ad42d0 100644 --- a/src/charging-station/Bootstrap.ts +++ b/src/charging-station/Bootstrap.ts @@ -81,13 +81,6 @@ export class Bootstrap extends EventEmitter { this.uiServer = UIServerFactory.getUIServerImplementation( Configuration.getConfigurationSection(ConfigurationSection.uiServer), ); - this.on(ChargingStationWorkerMessageEvents.started, this.workerEventStarted); - this.on(ChargingStationWorkerMessageEvents.stopped, this.workerEventStopped); - this.on(ChargingStationWorkerMessageEvents.updated, this.workerEventUpdated); - this.on( - ChargingStationWorkerMessageEvents.performanceStatistics, - this.workerEventPerformanceStatistics, - ); Configuration.configurationChangeCallback = async () => Bootstrap.getInstance().restart(false); } @@ -102,6 +95,13 @@ export class Bootstrap extends EventEmitter { if (this.started === false) { if (this.starting === false) { this.starting = true; + this.on(ChargingStationWorkerMessageEvents.started, this.workerEventStarted); + this.on(ChargingStationWorkerMessageEvents.stopped, this.workerEventStopped); + this.on(ChargingStationWorkerMessageEvents.updated, this.workerEventUpdated); + this.on( + ChargingStationWorkerMessageEvents.performanceStatistics, + this.workerEventPerformanceStatistics, + ); this.initializeCounters(); const workerConfiguration = Configuration.getConfigurationSection( ConfigurationSection.worker, @@ -229,6 +229,7 @@ export class Bootstrap extends EventEmitter { this.numberOfChargingStations, ) .then(() => { + this.removeAllListeners(); resolve('Charging stations stopped'); }) .catch(reject) diff --git a/src/utils/Configuration.ts b/src/utils/Configuration.ts index 1f370480..31d994f5 100644 --- a/src/utils/Configuration.ts +++ b/src/utils/Configuration.ts @@ -1,20 +1,21 @@ import { type FSWatcher, readFileSync, watch } from 'node:fs'; -import { dirname, join, resolve } from 'node:path'; +import { dirname, join } from 'node:path'; import { env } from 'node:process'; import { fileURLToPath } from 'node:url'; import chalk from 'chalk'; import merge from 'just-merge'; -import { Constants } from './Constants'; import { - hasOwnProp, - isCFEnvironment, - isNotEmptyString, - isUndefined, + buildPerformanceUriFilePath, + checkWorkerElementsPerWorker, + checkWorkerProcessType, + getDefaultPerformanceStorageUri, + handleFileException, logPrefix, - once, -} from './Utils'; +} from './ConfigurationUtils'; +import { Constants } from './Constants'; +import { hasOwnProp, isCFEnvironment, isUndefined, once } from './Utils'; import { ApplicationProtocol, type ConfigurationData, @@ -42,11 +43,6 @@ type ConfigurationSectionType = | WorkerConfiguration | UIServerConfiguration; -// Avoid ESM race condition at class initialization -const configurationLogPrefix = (): string => { - return logPrefix(' Simulator configuration |'); -}; - export class Configuration { public static configurationChangeCallback: () => Promise; @@ -183,7 +179,7 @@ export class Configuration { let storageConfiguration: StorageConfiguration = { enabled: false, type: StorageType.JSON_FILE, - uri: Configuration.getDefaultPerformanceStorageUri(StorageType.JSON_FILE), + uri: getDefaultPerformanceStorageUri(StorageType.JSON_FILE), }; if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.performanceStorage)) { storageConfiguration = { @@ -192,7 +188,7 @@ export class Configuration { ...(Configuration.getConfigurationData()?.performanceStorage?.type === StorageType.JSON_FILE && Configuration.getConfigurationData()?.performanceStorage?.uri && { - uri: Configuration.buildPerformanceUriFilePath( + uri: buildPerformanceUriFilePath( new URL(Configuration.getConfigurationData()!.performanceStorage!.uri!).pathname, ), }), @@ -289,11 +285,8 @@ export class Configuration { ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.worker) && Configuration.getConfigurationData()?.worker), }; - if (!Object.values(WorkerProcessType).includes(workerConfiguration.processType!)) { - throw new SyntaxError( - `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`, - ); - } + checkWorkerProcessType(workerConfiguration.processType!); + checkWorkerElementsPerWorker(workerConfiguration.elementsPerWorker); return workerConfiguration; } @@ -332,7 +325,7 @@ export class Configuration { (stationTemplateUrl: StationTemplateUrl) => { if (!isUndefined(stationTemplateUrl?.['numberOfStation' as keyof StationTemplateUrl])) { console.error( - `${chalk.green(configurationLogPrefix())} ${chalk.red( + `${chalk.green(logPrefix())} ${chalk.red( `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`, )}`, ); @@ -412,7 +405,7 @@ export class Configuration { ('staticPool' as WorkerProcessType) ) { console.error( - `${chalk.green(configurationLogPrefix())} ${chalk.red( + `${chalk.green(logPrefix())} ${chalk.red( `Deprecated configuration 'staticPool' value usage in worker section 'processType' field. Use '${WorkerProcessType.fixedPool}' value instead`, )}`, ); @@ -477,7 +470,7 @@ export class Configuration { // uiServer section if (hasOwnProp(Configuration.getConfigurationData(), 'uiWebSocketServer')) { console.error( - `${chalk.green(configurationLogPrefix())} ${chalk.red( + `${chalk.green(logPrefix())} ${chalk.red( `Deprecated configuration section 'uiWebSocketServer' usage. Use '${ConfigurationSection.uiServer}' instead`, )}`, ); @@ -504,7 +497,7 @@ export class Configuration { ) ) { console.error( - `${chalk.green(configurationLogPrefix())} ${chalk.red( + `${chalk.green(logPrefix())} ${chalk.red( `Deprecated configuration key '${key}' usage in section '${sectionName}'${ logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : '' }`, @@ -514,7 +507,7 @@ export class Configuration { !isUndefined(Configuration.getConfigurationData()?.[key as keyof ConfigurationData]) ) { console.error( - `${chalk.green(configurationLogPrefix())} ${chalk.red( + `${chalk.green(logPrefix())} ${chalk.red( `Deprecated configuration key '${key}' usage${ logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : '' }`, @@ -533,11 +526,11 @@ export class Configuration { Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher(); } } catch (error) { - Configuration.handleFileException( + handleFileException( Configuration.configurationFile, FileType.Configuration, error as NodeJS.ErrnoException, - configurationLogPrefix(), + logPrefix(), ); } } @@ -555,7 +548,7 @@ export class Configuration { Configuration.configurationFileReloading = true; const consoleWarnOnce = once(console.warn, this); consoleWarnOnce( - `${chalk.green(configurationLogPrefix())} ${chalk.yellow( + `${chalk.green(logPrefix())} ${chalk.yellow( `${FileType.Configuration} ${this.configurationFile} file have changed, reload`, )}`, ); @@ -575,59 +568,12 @@ export class Configuration { } }); } catch (error) { - Configuration.handleFileException( + handleFileException( Configuration.configurationFile, FileType.Configuration, error as NodeJS.ErrnoException, - configurationLogPrefix(), + logPrefix(), ); } } - - private static handleFileException( - file: string, - fileType: FileType, - error: NodeJS.ErrnoException, - logPfx: string, - ): void { - const prefix = isNotEmptyString(logPfx) ? `${logPfx} ` : ''; - let logMsg: string; - switch (error.code) { - case 'ENOENT': - logMsg = `${fileType} file ${file} not found: `; - break; - case 'EEXIST': - logMsg = `${fileType} file ${file} already exists: `; - break; - case 'EACCES': - logMsg = `${fileType} file ${file} access denied: `; - break; - case 'EPERM': - logMsg = `${fileType} file ${file} permission denied: `; - break; - default: - logMsg = `${fileType} file ${file} error: `; - } - console.error(`${chalk.green(prefix)}${chalk.red(logMsg)}`, error); - throw error; - } - - private static getDefaultPerformanceStorageUri(storageType: StorageType) { - switch (storageType) { - case StorageType.JSON_FILE: - return Configuration.buildPerformanceUriFilePath( - `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`, - ); - case StorageType.SQLITE: - return Configuration.buildPerformanceUriFilePath( - `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`, - ); - default: - throw new Error(`Unsupported storage type '${storageType}'`); - } - } - - private static buildPerformanceUriFilePath(file: string) { - return `file://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`; - } } diff --git a/src/utils/ConfigurationUtils.ts b/src/utils/ConfigurationUtils.ts new file mode 100644 index 00000000..eb992cad --- /dev/null +++ b/src/utils/ConfigurationUtils.ts @@ -0,0 +1,88 @@ +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import chalk from 'chalk'; + +import { Constants } from './Constants'; +import { isNotEmptyString, logPrefix as utilsLogPrefix } from './Utils'; +import { FileType, StorageType } from '../types'; +import { WorkerProcessType } from '../worker'; + +export const logPrefix = (): string => { + return utilsLogPrefix(' Simulator configuration |'); +}; + +export const buildPerformanceUriFilePath = (file: string) => { + return `file://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`; +}; + +export const getDefaultPerformanceStorageUri = (storageType: StorageType) => { + switch (storageType) { + case StorageType.JSON_FILE: + return buildPerformanceUriFilePath( + `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`, + ); + case StorageType.SQLITE: + return buildPerformanceUriFilePath( + `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`, + ); + default: + throw new Error(`Unsupported storage type '${storageType}'`); + } +}; + +export const handleFileException = ( + file: string, + fileType: FileType, + error: NodeJS.ErrnoException, + logPfx: string, +): void => { + const prefix = isNotEmptyString(logPfx) ? `${logPfx} ` : ''; + let logMsg: string; + switch (error.code) { + case 'ENOENT': + logMsg = `${fileType} file ${file} not found: `; + break; + case 'EEXIST': + logMsg = `${fileType} file ${file} already exists: `; + break; + case 'EACCES': + logMsg = `${fileType} file ${file} access denied: `; + break; + case 'EPERM': + logMsg = `${fileType} file ${file} permission denied: `; + break; + default: + logMsg = `${fileType} file ${file} error: `; + } + console.error(`${chalk.green(prefix)}${chalk.red(logMsg)}`, error); + throw error; +}; + +export const checkWorkerProcessType = (workerProcessType: WorkerProcessType): void => { + if (!Object.values(WorkerProcessType).includes(workerProcessType)) { + throw new SyntaxError( + `Invalid worker process type '${workerProcessType}' defined in configuration`, + ); + } +}; + +export const checkWorkerElementsPerWorker = ( + elementsPerWorker: number | 'auto' | 'single' | undefined, +): void => { + if ( + elementsPerWorker !== undefined && + elementsPerWorker !== 'auto' && + elementsPerWorker !== 'single' && + !Number.isSafeInteger(elementsPerWorker) + ) { + throw new SyntaxError( + `Invalid number of elements per worker '${elementsPerWorker}' defined in configuration`, + ); + } + if (Number.isSafeInteger(elementsPerWorker) && (elementsPerWorker as number) <= 0) { + throw RangeError( + `Invalid negative or zero number of elements per worker '${elementsPerWorker}' defined in configuration`, + ); + } +}; -- 2.34.1