From d17dffd1e1c266609f83feb9edccff5db05ca896 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Thu, 14 Aug 2025 00:03:32 +0200 Subject: [PATCH] fix: potential event handler leaking MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- src/charging-station/ChargingStation.ts | 2 +- src/charging-station/Helpers.ts | 88 +++++++++---------- .../ocpp/1.6/OCPP16ServiceUtils.ts | 2 +- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 5dca7b72..7b6281dd 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -1868,7 +1868,7 @@ export class ChargingStation extends EventEmitter { } if ( getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null && - hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true + hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) ) { addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false') } diff --git a/src/charging-station/Helpers.ts b/src/charging-station/Helpers.ts index cadae37b..aa34494f 100644 --- a/src/charging-station/Helpers.ts +++ b/src/charging-station/Helpers.ts @@ -17,7 +17,7 @@ import { toDate, } from 'date-fns' import { maxTime } from 'date-fns/constants' -import { hash, randomBytes } from 'node:crypto' +import { createHash, randomBytes } from 'node:crypto' import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path' import { env } from 'node:process' import { fileURLToPath } from 'node:url' @@ -110,6 +110,7 @@ export const hasReservationExpired = (reservation: Reservation): boolean => { export const removeExpiredReservations = async ( chargingStation: ChargingStation ): Promise => { + const reservations: Reservation[] = [] if (chargingStation.hasEvses) { for (const evseStatus of chargingStation.evses.values()) { for (const connectorStatus of evseStatus.connectors.values()) { @@ -117,10 +118,7 @@ export const removeExpiredReservations = async ( connectorStatus.reservation != null && hasReservationExpired(connectorStatus.reservation) ) { - await chargingStation.removeReservation( - connectorStatus.reservation, - ReservationTerminationReason.EXPIRED - ) + reservations.push(connectorStatus.reservation) } } } @@ -130,13 +128,22 @@ export const removeExpiredReservations = async ( connectorStatus.reservation != null && hasReservationExpired(connectorStatus.reservation) ) { - await chargingStation.removeReservation( - connectorStatus.reservation, - ReservationTerminationReason.EXPIRED - ) + reservations.push(connectorStatus.reservation) } } } + const results = await Promise.allSettled( + reservations.map(reservation => + chargingStation.removeReservation(reservation, ReservationTerminationReason.EXPIRED) + ) + ) + for (const result of results) { + if (result.status === 'rejected') { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.removeExpiredReservations: reservation removal failed: ${String(result.reason)}` + ) + } + } } export const getNumberOfReservableConnectors = ( @@ -171,11 +178,9 @@ export const getHashId = (index: number, stationTemplate: ChargingStationTemplat meterType: stationTemplate.meterType, }), } - return hash( - Constants.DEFAULT_HASH_ALGORITHM, - `${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`, - 'hex' - ) + return createHash(Constants.DEFAULT_HASH_ALGORITHM) + .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`) + .digest('hex') } export const validateStationInfo = (chargingStation: ChargingStation): void => { @@ -265,6 +270,7 @@ export const getPhaseRotationValue = ( } else if (connectorId >= 0 && numberOfPhases === 3) { return `${connectorId.toString()}.${ConnectorPhaseRotation.RST}` } + return undefined } export const getMaxNumberOfEvses = (evses: Record | undefined): number => { @@ -430,9 +436,8 @@ export const buildConnectorsMap = ( ): Map => { const connectorsMap = new Map() if (getMaxNumberOfConnectors(connectors) > 0) { - for (const connector in connectors) { - const connectorStatus = connectors[connector] - const connectorId = convertToInt(connector) + for (const [connectorKey, connectorStatus] of Object.entries(connectors)) { + const connectorId = convertToInt(connectorKey) checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile) connectorsMap.set(connectorId, clone(connectorStatus)) } @@ -448,25 +453,18 @@ export const initializeConnectorsMapStatus = ( connectors: Map, logPrefix: string ): void => { - for (const connectorId of connectors.keys()) { - if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) { + for (const [connectorId, connectorStatus] of connectors) { + if (connectorId > 0 && connectorStatus.transactionStarted === true) { logger.warn( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${logPrefix} Connector id ${connectorId.toString()} at initialization has a transaction started with id ${connectors - .get(connectorId) - ?.transactionId?.toString()}` + `${logPrefix} Connector id ${connectorId.toString()} at initialization has a transaction started with id ${connectorStatus.transactionId?.toString()}` ) } if (connectorId === 0) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - connectors.get(connectorId)!.availability = AvailabilityType.Operative - if (connectors.get(connectorId)?.chargingProfiles == null) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - connectors.get(connectorId)!.chargingProfiles = [] - } - } else if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted == null) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - initializeConnectorStatus(connectors.get(connectorId)!) + connectorStatus.availability = AvailabilityType.Operative + connectorStatus.chargingProfiles ??= [] + } else if (connectorId > 0 && connectorStatus.transactionStarted == null) { + initializeConnectorStatus(connectorStatus) } } } @@ -504,8 +502,12 @@ export const resetConnectorStatus = (connectorStatus: ConnectorStatus | undefine export const prepareConnectorStatus = (connectorStatus: ConnectorStatus): ConnectorStatus => { if (connectorStatus.reservation != null) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - connectorStatus.reservation.expiryDate = convertToDate(connectorStatus.reservation.expiryDate)! + const reservationExpiryDate = convertToDate(connectorStatus.reservation.expiryDate) + if (reservationExpiryDate != null) { + connectorStatus.reservation.expiryDate = reservationExpiryDate + } else { + delete connectorStatus.reservation + } } if (isNotEmptyArray(connectorStatus.chargingProfiles)) { connectorStatus.chargingProfiles = connectorStatus.chargingProfiles @@ -669,8 +671,8 @@ export const propagateSerialNumber = ( export const hasFeatureProfile = ( chargingStation: ChargingStation, featureProfile: SupportedFeatureProfiles -): boolean | undefined => { - return getConfigurationKey( +): boolean => { + return !!getConfigurationKey( chargingStation, StandardParametersKey.SupportedFeatureProfiles )?.value?.includes(featureProfile) @@ -860,12 +862,14 @@ export const waitChargingStationEvents = async ( resolve(events) return } - emitter.on(event, () => { + const handler = () => { ++events if (events === eventsToWait) { + emitter.off(event, handler) resolve(events) } - }) + } + emitter.on(event, handler) }) } @@ -884,13 +888,9 @@ const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemp ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1 : getMaxNumberOfConnectors(stationTemplate.Connectors) } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) { - for (const evse in stationTemplate.Evses) { - if (evse === '0') { - continue - } - configuredMaxNumberOfConnectors += getMaxNumberOfConnectors( - stationTemplate.Evses[evse].Connectors - ) + for (const [evseId, evseStatus] of Object.entries(stationTemplate.Evses)) { + if (evseId === '0') continue + configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(evseStatus.Connectors) } } return configuredMaxNumberOfConnectors diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index dba9fbf9..84467285 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -116,7 +116,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { featureProfile: OCPP16SupportedFeatureProfiles, command: OCPP16IncomingRequestCommand | OCPP16RequestCommand ): boolean { - if (hasFeatureProfile(chargingStation, featureProfile) === false) { + if (!hasFeatureProfile(chargingStation, featureProfile)) { logger.warn( `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${ OCPP16StandardParametersKey.SupportedFeatureProfiles -- 2.43.0