From 6c8f5d901f6dbfd66e921decde63bf73548c156e Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 24 Aug 2022 00:34:52 +0200 Subject: [PATCH] Implement error handling and propagation in IPC and UI server code 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/ChargingStationUtils.ts | 4 +- .../ChargingStationWorkerBroadcastChannel.ts | 82 ++++++++++++++++--- .../UIServiceWorkerBroadcastChannel.ts | 33 ++++++++ .../WorkerBroadcastChannel.ts | 14 +++- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 5 +- .../ocpp/1.6/OCPP16RequestService.ts | 4 +- .../ocpp/1.6/OCPP16ResponseService.ts | 9 +- .../ui-server/UIWebSocketServer.ts | 14 +--- .../ui-services/AbstractUIService.ts | 61 ++++++++------ .../ui-server/ui-services/UIService001.ts | 14 +--- src/types/UIProtocol.ts | 2 +- src/types/WorkerBroadcastChannel.ts | 9 +- 13 files changed, 178 insertions(+), 75 deletions(-) create mode 100644 src/charging-station/UIServiceWorkerBroadcastChannel.ts diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 4ed4e600..0c00bf20 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -856,7 +856,7 @@ export default class ChargingStation { this.templateFile }`; logger.error(errMsg); - throw new Error(errMsg); + throw new BaseError(errMsg); } private initialize(): void { diff --git a/src/charging-station/ChargingStationUtils.ts b/src/charging-station/ChargingStationUtils.ts index 2d6f8464..38dc60c3 100644 --- a/src/charging-station/ChargingStationUtils.ts +++ b/src/charging-station/ChargingStationUtils.ts @@ -437,7 +437,7 @@ export class ChargingStationUtils { break; default: logger.error(errMsg); - throw new Error(errMsg); + throw new BaseError(errMsg); } return defaultVoltageOut; } @@ -516,7 +516,7 @@ export class ChargingStationUtils { if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) { const errorMsg = `${chargingStation.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`; logger.error(errorMsg); - throw new Error(errorMsg); + throw new BaseError(errorMsg); } logger.debug( `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}` diff --git a/src/charging-station/ChargingStationWorkerBroadcastChannel.ts b/src/charging-station/ChargingStationWorkerBroadcastChannel.ts index 6d38f06c..0d890687 100644 --- a/src/charging-station/ChargingStationWorkerBroadcastChannel.ts +++ b/src/charging-station/ChargingStationWorkerBroadcastChannel.ts @@ -1,5 +1,7 @@ +import BaseError from '../exception/BaseError'; import { RequestCommand } from '../types/ocpp/Requests'; import { + AuthorizationStatus, StartTransactionRequest, StartTransactionResponse, StopTransactionReason, @@ -9,13 +11,18 @@ import { import { BroadcastChannelProcedureName, BroadcastChannelRequest, + BroadcastChannelRequestPayload, + BroadcastChannelResponsePayload, + MessageEvent, } from '../types/WorkerBroadcastChannel'; +import { ResponseStatus } from '../ui/web/src/type/UIProtocol'; +import logger from '../utils/Logger'; import ChargingStation from './ChargingStation'; import WorkerBroadcastChannel from './WorkerBroadcastChannel'; const moduleName = 'ChargingStationWorkerBroadcastChannel'; -type MessageEvent = { data: unknown }; +type CommandResponse = StartTransactionResponse | StopTransactionResponse; export default class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel { private readonly chargingStation: ChargingStation; @@ -24,45 +31,94 @@ export default class ChargingStationWorkerBroadcastChannel extends WorkerBroadca super(); this.chargingStation = chargingStation; this.onmessage = this.requestHandler.bind(this) as (message: MessageEvent) => void; + this.onmessageerror = this.messageErrorHandler.bind(this) as (message: MessageEvent) => void; } private async requestHandler(messageEvent: MessageEvent): Promise { - const [, command, payload] = messageEvent.data as BroadcastChannelRequest; + if (this.isResponse(messageEvent.data)) { + return; + } - if (payload.hashId !== this.chargingStation.hashId) { + const [uuid, command, requestPayload] = messageEvent.data as BroadcastChannelRequest; + + if (requestPayload?.hashId !== this.chargingStation.hashId) { return; } - // TODO: return a response stating the command success or failure + let responsePayload: BroadcastChannelResponsePayload; + let commandResponse: CommandResponse; + try { + commandResponse = await this.commandHandler(command, requestPayload); + if (commandResponse === undefined) { + responsePayload = { status: ResponseStatus.SUCCESS }; + } else { + responsePayload = { status: this.commandResponseToResponseStatus(commandResponse) }; + } + } catch (error) { + logger.error( + `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`, + error + ); + responsePayload = { + status: ResponseStatus.FAILURE, + command, + requestPayload, + commandResponse, + errorMessage: (error as Error).message, + errorStack: (error as Error).stack, + }; + } + this.sendResponse([uuid, responsePayload]); + } + + private messageErrorHandler(messageEvent: MessageEvent): void { + logger.error( + `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`, + { messageEvent, messageEventData: messageEvent.data } + ); + } + + private async commandHandler( + command: BroadcastChannelProcedureName, + requestPayload: BroadcastChannelRequestPayload + ): Promise { switch (command) { case BroadcastChannelProcedureName.START_TRANSACTION: - await this.chargingStation.ocppRequestService.requestHandler< + return this.chargingStation.ocppRequestService.requestHandler< StartTransactionRequest, StartTransactionResponse >(this.chargingStation, RequestCommand.START_TRANSACTION, { - connectorId: payload.connectorId, - idTag: payload.idTag, + connectorId: requestPayload.connectorId, + idTag: requestPayload.idTag, }); - break; case BroadcastChannelProcedureName.STOP_TRANSACTION: - await this.chargingStation.ocppRequestService.requestHandler< + return this.chargingStation.ocppRequestService.requestHandler< StopTransactionRequest, StopTransactionResponse >(this.chargingStation, RequestCommand.STOP_TRANSACTION, { - transactionId: payload.transactionId, + transactionId: requestPayload.transactionId, meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId( - payload.transactionId + requestPayload.transactionId ), - idTag: this.chargingStation.getTransactionIdTag(payload.transactionId), + idTag: this.chargingStation.getTransactionIdTag(requestPayload.transactionId), reason: StopTransactionReason.NONE, }); - break; case BroadcastChannelProcedureName.START_CHARGING_STATION: this.chargingStation.start(); break; case BroadcastChannelProcedureName.STOP_CHARGING_STATION: await this.chargingStation.stop(); break; + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new BaseError(`Unknown broadcast channel command: ${command}`); + } + } + + private commandResponseToResponseStatus(commandResponse: CommandResponse): ResponseStatus { + if (commandResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) { + return ResponseStatus.SUCCESS; } + return ResponseStatus.FAILURE; } } diff --git a/src/charging-station/UIServiceWorkerBroadcastChannel.ts b/src/charging-station/UIServiceWorkerBroadcastChannel.ts new file mode 100644 index 00000000..e921972d --- /dev/null +++ b/src/charging-station/UIServiceWorkerBroadcastChannel.ts @@ -0,0 +1,33 @@ +import { BroadcastChannelResponse, MessageEvent } from '../types/WorkerBroadcastChannel'; +import logger from '../utils/Logger'; +import AbstractUIService from './ui-server/ui-services/AbstractUIService'; +import WorkerBroadcastChannel from './WorkerBroadcastChannel'; + +const moduleName = 'UIServiceWorkerBroadcastChannel'; + +export default class UIServiceWorkerBroadcastChannel extends WorkerBroadcastChannel { + private uiService: AbstractUIService; + + constructor(uiService: AbstractUIService) { + super(); + this.uiService = uiService; + this.onmessage = this.responseHandler.bind(this) as (message: MessageEvent) => void; + this.onmessageerror = this.messageErrorHandler.bind(this) as (message: MessageEvent) => void; + } + + private responseHandler(messageEvent: MessageEvent): void { + if (this.isRequest(messageEvent.data)) { + return; + } + const [uuid, responsePayload] = messageEvent.data as BroadcastChannelResponse; + + this.uiService.sendResponse(uuid, responsePayload); + } + + private messageErrorHandler(messageEvent: MessageEvent): void { + logger.error( + `${this.uiService.logPrefix(moduleName, 'messageErrorHandler')} Error at handling message:`, + { messageEvent, messageEventData: messageEvent.data } + ); + } +} diff --git a/src/charging-station/WorkerBroadcastChannel.ts b/src/charging-station/WorkerBroadcastChannel.ts index 2099d695..12a877c1 100644 --- a/src/charging-station/WorkerBroadcastChannel.ts +++ b/src/charging-station/WorkerBroadcastChannel.ts @@ -2,8 +2,8 @@ import { BroadcastChannel } from 'worker_threads'; import { BroadcastChannelRequest, BroadcastChannelResponse } from '../types/WorkerBroadcastChannel'; -export default class WorkerBroadcastChannel extends BroadcastChannel { - constructor() { +export default abstract class WorkerBroadcastChannel extends BroadcastChannel { + protected constructor() { super('worker'); } @@ -11,7 +11,15 @@ export default class WorkerBroadcastChannel extends BroadcastChannel { this.postMessage(request); } - public sendResponse(response: BroadcastChannelResponse): void { + protected sendResponse(response: BroadcastChannelResponse): void { this.postMessage(response); } + + protected isRequest(message: any): boolean { + return Array.isArray(message) && message.length === 3; + } + + protected isResponse(message: any): boolean { + return Array.isArray(message) && message.length === 2; + } } diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index b785db03..298d6af2 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -320,7 +320,10 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer ); } catch (error) { // Log - logger.error(chargingStation.logPrefix() + ' Handle request error:', error); + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`, + error + ); throw error; } } else { diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index 1f8e58d5..a0f09bb8 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -167,7 +167,7 @@ export default class OCPP16RequestService extends OCPPRequestService { } throw new OCPPError( ErrorType.NOT_SUPPORTED, - `${moduleName}.requestHandler: Unsupported OCPP command '${commandName}'`, + `Unsupported OCPP command '${commandName}'`, commandName, commandParams ); @@ -263,7 +263,7 @@ export default class OCPP16RequestService extends OCPPRequestService { throw new OCPPError( ErrorType.NOT_SUPPORTED, // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${moduleName}.buildRequestPayload: Unsupported OCPP command '${commandName}'`, + `Unsupported OCPP command '${commandName}'`, commandName, commandParams ); diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index dc68dd74..cf7e729e 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -183,14 +183,17 @@ export default class OCPP16ResponseService extends OCPPResponseService { this.validatePayload(chargingStation, commandName, payload); await this.responseHandlers.get(commandName)(chargingStation, payload, requestPayload); } catch (error) { - logger.error(chargingStation.logPrefix() + ' Handle request response error:', error); + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`, + error + ); throw error; } } else { // Throw exception throw new OCPPError( ErrorType.NOT_IMPLEMENTED, - `${commandName} is not implemented to handle request response PDU ${JSON.stringify( + `${commandName} is not implemented to handle response PDU ${JSON.stringify( payload, null, 2 @@ -202,7 +205,7 @@ export default class OCPP16ResponseService extends OCPPResponseService { } else { throw new OCPPError( ErrorType.SECURITY_ERROR, - `${commandName} cannot be issued to handle request response PDU ${JSON.stringify( + `${commandName} cannot be issued to handle response PDU ${JSON.stringify( payload, null, 2 diff --git a/src/charging-station/ui-server/UIWebSocketServer.ts b/src/charging-station/ui-server/UIWebSocketServer.ts index 66980f61..5f02f2ea 100644 --- a/src/charging-station/ui-server/UIWebSocketServer.ts +++ b/src/charging-station/ui-server/UIWebSocketServer.ts @@ -28,18 +28,12 @@ export default class UIWebSocketServer extends AbstractUIServer { this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this)); } // FIXME: check connection validity - socket.on('message', (messageData) => { + socket.on('message', (rawData) => { this.uiServices .get(version) - .requestHandler(messageData) - .catch((error) => { - logger.error( - `${this.logPrefix( - moduleName, - 'start.socket.onmessage' - )} Error while handling message:`, - error - ); + .requestHandler(rawData) + .catch(() => { + /* Error caught by AbstractUIService */ }); }); socket.on('error', (error) => { diff --git a/src/charging-station/ui-server/ui-services/AbstractUIService.ts b/src/charging-station/ui-server/ui-services/AbstractUIService.ts index 805bda20..9c1525dc 100644 --- a/src/charging-station/ui-server/ui-services/AbstractUIService.ts +++ b/src/charging-station/ui-server/ui-services/AbstractUIService.ts @@ -15,7 +15,7 @@ import { import logger from '../../../utils/Logger'; import Utils from '../../../utils/Utils'; import Bootstrap from '../../Bootstrap'; -import WorkerBroadcastChannel from '../../WorkerBroadcastChannel'; +import UIServiceWorkerBroadcastChannel from '../../UIServiceWorkerBroadcastChannel'; import { AbstractUIServer } from '../AbstractUIServer'; const moduleName = 'AbstractUIService'; @@ -24,7 +24,7 @@ export default abstract class AbstractUIService { protected readonly version: ProtocolVersion; protected readonly uiServer: AbstractUIServer; protected readonly requestHandlers: Map; - protected workerBroadcastChannel: WorkerBroadcastChannel; + protected workerBroadcastChannel: UIServiceWorkerBroadcastChannel; constructor(uiServer: AbstractUIServer, version: ProtocolVersion) { this.version = version; @@ -34,13 +34,13 @@ export default abstract class AbstractUIService { [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)], [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)], ]); - this.workerBroadcastChannel = new WorkerBroadcastChannel(); + this.workerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this); } public async requestHandler(request: RawData): Promise { let messageId: string; let command: ProcedureName; - let requestPayload: RequestPayload; + let requestPayload: RequestPayload | undefined; let responsePayload: ResponsePayload; try { [messageId, command, requestPayload] = this.requestValidation(request); @@ -55,41 +55,56 @@ export default abstract class AbstractUIService { ); } - // Call the message handler to build the response payload + // Call the request handler to build the response payload responsePayload = await this.requestHandlers.get(command)(messageId, requestPayload); } catch (error) { // Log logger.error( - `${this.uiServer.logPrefix(moduleName, 'messageHandler')} Handle message error:`, + `${this.uiServer.logPrefix(moduleName, 'messageHandler')} Handle request error:`, error ); - // Send the message response failure - this.uiServer.sendResponse( - this.buildProtocolResponse(messageId ?? 'error', { - status: ResponseStatus.FAILURE, - command, - requestPayload, - errorMessage: (error as Error).message, - errorStack: (error as Error).stack, - }) - ); - throw error; + responsePayload = { + status: ResponseStatus.FAILURE, + command, + requestPayload, + responsePayload, + errorMessage: (error as Error).message, + errorStack: (error as Error).stack, + }; + } + + if (responsePayload !== undefined) { + // Send the response + this.uiServer.sendResponse(this.buildProtocolResponse(messageId ?? 'error', responsePayload)); } + } + + public sendRequest( + messageId: string, + procedureName: ProcedureName, + requestPayload: RequestPayload + ): void { + this.uiServer.sendRequest(this.buildProtocolRequest(messageId, procedureName, requestPayload)); + } - // Send the message response success + public sendResponse(messageId: string, responsePayload: ResponsePayload): void { this.uiServer.sendResponse(this.buildProtocolResponse(messageId, responsePayload)); } - protected buildProtocolRequest( + public logPrefix(modName: string, methodName: string): string { + return `${this.uiServer.logPrefix(modName, methodName)}`; + } + + private buildProtocolRequest( messageId: string, procedureName: ProcedureName, - payload: RequestPayload + requestPayload: RequestPayload ): string { - return JSON.stringify([messageId, procedureName, payload] as ProtocolRequest); + return JSON.stringify([messageId, procedureName, requestPayload] as ProtocolRequest); } - protected buildProtocolResponse(messageId: string, payload: ResponsePayload): string { - return JSON.stringify([messageId, payload] as ProtocolResponse); + private buildProtocolResponse(messageId: string, responsePayload: ResponsePayload): string { + return JSON.stringify([messageId, responsePayload] as ProtocolResponse); } // Validate the raw data received from the WebSocket diff --git a/src/charging-station/ui-server/ui-services/UIService001.ts b/src/charging-station/ui-server/ui-services/UIService001.ts index 5c9b7dc4..88a02bba 100644 --- a/src/charging-station/ui-server/ui-services/UIService001.ts +++ b/src/charging-station/ui-server/ui-services/UIService001.ts @@ -3,8 +3,6 @@ import { ProtocolRequestHandler, ProtocolVersion, RequestPayload, - ResponsePayload, - ResponseStatus, } from '../../../types/UIProtocol'; import { BroadcastChannelProcedureName, @@ -34,39 +32,35 @@ export default class UIService001 extends AbstractUIService { ); } - private handleStartTransaction(uuid: string, payload: RequestPayload): ResponsePayload { + private handleStartTransaction(uuid: string, payload: RequestPayload): void { this.workerBroadcastChannel.sendRequest([ uuid, BroadcastChannelProcedureName.START_TRANSACTION, payload as BroadcastChannelRequestPayload, ]); - return { status: ResponseStatus.SUCCESS }; } - private handleStopTransaction(uuid: string, payload: RequestPayload): ResponsePayload { + private handleStopTransaction(uuid: string, payload: RequestPayload): void { this.workerBroadcastChannel.sendRequest([ uuid, BroadcastChannelProcedureName.STOP_TRANSACTION, payload as BroadcastChannelRequestPayload, ]); - return { status: ResponseStatus.SUCCESS }; } - private handleStartChargingStation(uuid: string, payload: RequestPayload): ResponsePayload { + private handleStartChargingStation(uuid: string, payload: RequestPayload): void { this.workerBroadcastChannel.sendRequest([ uuid, BroadcastChannelProcedureName.START_CHARGING_STATION, payload as BroadcastChannelRequestPayload, ]); - return { status: ResponseStatus.SUCCESS }; } - private handleStopChargingStation(uuid: string, payload: RequestPayload): ResponsePayload { + private handleStopChargingStation(uuid: string, payload: RequestPayload): void { this.workerBroadcastChannel.sendRequest([ uuid, BroadcastChannelProcedureName.STOP_CHARGING_STATION, payload as BroadcastChannelRequestPayload, ]); - return { status: ResponseStatus.SUCCESS }; } } diff --git a/src/types/UIProtocol.ts b/src/types/UIProtocol.ts index 2c591495..27585b03 100644 --- a/src/types/UIProtocol.ts +++ b/src/types/UIProtocol.ts @@ -19,7 +19,7 @@ export type ProtocolResponse = [string, ResponsePayload]; export type ProtocolRequestHandler = ( uuid?: string, payload?: RequestPayload -) => ResponsePayload | Promise; +) => undefined | Promise | ResponsePayload | Promise; export enum ProcedureName { LIST_CHARGING_STATIONS = 'listChargingStations', diff --git a/src/types/WorkerBroadcastChannel.ts b/src/types/WorkerBroadcastChannel.ts index 9d1133d0..610d1c45 100644 --- a/src/types/WorkerBroadcastChannel.ts +++ b/src/types/WorkerBroadcastChannel.ts @@ -15,16 +15,13 @@ export enum BroadcastChannelProcedureName { STOP_TRANSACTION = 'stopTransaction', } -interface BroadcastChannelBasePayload extends JsonObject { +export interface BroadcastChannelRequestPayload extends Omit { hashId: string; -} - -export interface BroadcastChannelRequestPayload - extends BroadcastChannelBasePayload, - Omit { connectorId?: number; transactionId?: number; idTag?: string; } export type BroadcastChannelResponsePayload = ResponsePayload; + +export type MessageEvent = { data: BroadcastChannelRequest | BroadcastChannelResponse }; -- 2.34.1