Implement error handling and propagation in IPC and UI server code
authorJérôme Benoit <jerome.benoit@sap.com>
Tue, 23 Aug 2022 22:34:52 +0000 (00:34 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Tue, 23 Aug 2022 22:34:52 +0000 (00:34 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
13 files changed:
src/charging-station/ChargingStation.ts
src/charging-station/ChargingStationUtils.ts
src/charging-station/ChargingStationWorkerBroadcastChannel.ts
src/charging-station/UIServiceWorkerBroadcastChannel.ts [new file with mode: 0644]
src/charging-station/WorkerBroadcastChannel.ts
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/charging-station/ocpp/1.6/OCPP16RequestService.ts
src/charging-station/ocpp/1.6/OCPP16ResponseService.ts
src/charging-station/ui-server/UIWebSocketServer.ts
src/charging-station/ui-server/ui-services/AbstractUIService.ts
src/charging-station/ui-server/ui-services/UIService001.ts
src/types/UIProtocol.ts
src/types/WorkerBroadcastChannel.ts

index 4ed4e600a9cd412e5f4335b648481d71a605b8a9..0c00bf20dd2936519e7415b5c9cfa96f9b43591e 100644 (file)
@@ -856,7 +856,7 @@ export default class ChargingStation {
       this.templateFile
     }`;
     logger.error(errMsg);
-    throw new Error(errMsg);
+    throw new BaseError(errMsg);
   }
 
   private initialize(): void {
index 2d6f84644e901cbc546ecf3f0eb3e59449277450..38dc60c37b3275e53553194894074e860758bd0f 100644 (file)
@@ -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}`
index 6d38f06c0f12539bccd17daa78c47492efc6ac45..0d890687828cb6b86ac450c48a3610f5e283c893 100644 (file)
@@ -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<void> {
-    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<CommandResponse | undefined> {
     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 (file)
index 0000000..e921972
--- /dev/null
@@ -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 }
+    );
+  }
+}
index 2099d695ab683e75d315151e02c9c5914eb6cedd..12a877c12add623365c0c8c6c6068c62841000d2 100644 (file)
@@ -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;
+  }
 }
index b785db03574aaf6afcd10dea5676533153d0a56e..298d6af2a7945501b13b07fed95fe43aa1c0c3a7 100644 (file)
@@ -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 {
index 1f8e58d5748c1db5cdf0e338ef08597b2033234f..a0f09bb8336d76ce442b88e26056829a0e63c12e 100644 (file)
@@ -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
         );
index dc68dd74beb7b2b342af9bd56c5ee0f6b5586bd3..cf7e729e09c2cafdeece6771f5b7d3a3dde7f912 100644 (file)
@@ -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
index 66980f610f08e86a4f111ded7db26a9bab7ec41e..5f02f2ea47f9de93276970d53672e078a7fbc9b8 100644 (file)
@@ -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) => {
index 805bda20dbd704744ce8503a77116895d8874af6..9c1525dc0aa6df441c0a726d32c3fa09e4f84593 100644 (file)
@@ -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<ProcedureName, ProtocolRequestHandler>;
-  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<void> {
     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
index 5c9b7dc4dc4525dd177cb1040879d1f281c791c3..88a02bbac96fe48b942949b70ea090b0dd623ed4 100644 (file)
@@ -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 };
   }
 }
index 2c5914953b2768bd94bdf836bb15c50fd40d1457..27585b03dad549cd58fcd0500b9346f88803314c 100644 (file)
@@ -19,7 +19,7 @@ export type ProtocolResponse = [string, ResponsePayload];
 export type ProtocolRequestHandler = (
   uuid?: string,
   payload?: RequestPayload
-) => ResponsePayload | Promise<ResponsePayload>;
+) => undefined | Promise<undefined> | ResponsePayload | Promise<ResponsePayload>;
 
 export enum ProcedureName {
   LIST_CHARGING_STATIONS = 'listChargingStations',
index 9d1133d0941d7dcc692797a8cb2528682b28ab8f..610d1c4517e9540505f53d565947657e8f647fab 100644 (file)
@@ -15,16 +15,13 @@ export enum BroadcastChannelProcedureName {
   STOP_TRANSACTION = 'stopTransaction',
 }
 
-interface BroadcastChannelBasePayload extends JsonObject {
+export interface BroadcastChannelRequestPayload extends Omit<RequestPayload, 'hashId'> {
   hashId: string;
-}
-
-export interface BroadcastChannelRequestPayload
-  extends BroadcastChannelBasePayload,
-    Omit<RequestPayload, 'hashId'> {
   connectorId?: number;
   transactionId?: number;
   idTag?: string;
 }
 
 export type BroadcastChannelResponsePayload = ResponsePayload;
+
+export type MessageEvent = { data: BroadcastChannelRequest | BroadcastChannelResponse };