From: Jérôme Benoit Date: Tue, 30 Nov 2021 20:50:41 +0000 (+0100) Subject: Move the UI WS protocol version handling is protocol header X-Git-Tag: v1.1.36~9 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=4198ad5c76ad58e444c1dd2d3f81b5f8fd2846d4;p=e-mobility-charging-stations-simulator.git Move the UI WS protocol version handling is protocol header Signed-off-by: Jérôme Benoit --- diff --git a/src/charging-station/Bootstrap.ts b/src/charging-station/Bootstrap.ts index fe344407..74fb74b7 100644 --- a/src/charging-station/Bootstrap.ts +++ b/src/charging-station/Bootstrap.ts @@ -5,8 +5,9 @@ import { ChargingStationWorkerData, ChargingStationWorkerMessage, ChargingStatio import Configuration from '../utils/Configuration'; import { Storage } from '../performance/storage/Storage'; import { StorageFactory } from '../performance/storage/StorageFactory'; +import { UIServiceUtils } from './UIWebSocketServices/UIServiceUtils'; +import UIWebSocketServer from './UIWebSocketServer'; import Utils from '../utils/Utils'; -import WebSocketServer from './WebSocketServer'; import WorkerAbstract from '../worker/WorkerAbstract'; import WorkerFactory from '../worker/WorkerFactory'; import chalk from 'chalk'; @@ -17,7 +18,7 @@ import { version } from '../../package.json'; export default class Bootstrap { private static instance: Bootstrap | null = null; private workerImplementation: WorkerAbstract | null = null; - private readonly webSocketServer: WebSocketServer; + private readonly uiWebSocketServer: UIWebSocketServer; private readonly storage: Storage; private numberOfChargingStations: number; private readonly version: string = version; @@ -28,7 +29,7 @@ export default class Bootstrap { this.started = false; this.workerScript = path.join(path.resolve(__dirname, '../'), 'charging-station', 'ChargingStationWorker.js'); this.initWorkerImplementation(); - this.webSocketServer = new WebSocketServer(); + this.uiWebSocketServer = new UIWebSocketServer({ port: 80, handleProtocols: UIServiceUtils.handleProtocols }); this.storage = StorageFactory.getStorage(Configuration.getPerformanceStorage().type, Configuration.getPerformanceStorage().URI, this.logPrefix()); Configuration.setConfigurationChangeCallback(async () => Bootstrap.getInstance().restart()); } @@ -46,7 +47,7 @@ export default class Bootstrap { this.numberOfChargingStations = 0; await this.storage.open(); await this.workerImplementation.start(); - this.webSocketServer.start(); + this.uiWebSocketServer.start(); // Start ChargingStation object in worker thread if (Configuration.getStationTemplateURLs()) { for (const stationURL of Configuration.getStationTemplateURLs()) { @@ -84,7 +85,7 @@ export default class Bootstrap { public async stop(): Promise { if (isMainThread && this.started) { await this.workerImplementation.stop(); - this.webSocketServer.stop(); + this.uiWebSocketServer.stop(); await this.storage.close(); } else { console.error(chalk.red('Trying to stop the charging stations simulator while not started')); @@ -110,9 +111,9 @@ export default class Bootstrap { }, messageHandler: async (msg: ChargingStationWorkerMessage) => { if (msg.id === ChargingStationWorkerMessageEvents.STARTED) { - this.webSocketServer.webSocketServerService.chargingStations.add(msg.data.id); + this.uiWebSocketServer.uiService.chargingStations.add(msg.data.id); } else if (msg.id === ChargingStationWorkerMessageEvents.STOPPED) { - this.webSocketServer.webSocketServerService.chargingStations.delete(msg.data.id); + this.uiWebSocketServer.uiService.chargingStations.delete(msg.data.id); } else if (msg.id === ChargingStationWorkerMessageEvents.PERFORMANCE_STATISTICS) { await this.storage.storePerformanceStatistics(msg.data); } diff --git a/src/charging-station/UIWebSocketServer.ts b/src/charging-station/UIWebSocketServer.ts new file mode 100644 index 00000000..78ca7516 --- /dev/null +++ b/src/charging-station/UIWebSocketServer.ts @@ -0,0 +1,61 @@ +import { Protocol, ProtocolCommand, ProtocolRequest, ProtocolVersion } from '../types/UIProtocol'; + +import AbstractUIService from './UIWebSocketServices/AbstractUIService'; +import BaseError from '../exception/BaseError'; +import { IncomingMessage } from 'http'; +import UIServiceFactory from './UIWebSocketServices/UIServiceFactory'; +import Utils from '../utils/Utils'; +import WebSocket from 'ws'; +import logger from '../utils/Logger'; + +export default class UIWebSocketServer extends WebSocket.Server { + public uiService: AbstractUIService; + + public constructor(options?: WebSocket.ServerOptions, callback?: () => void) { + // Create the WebSocket Server + super(options ?? { port: 80 }, callback); + } + + public broadcastToClients(message: string | Record): void { + for (const client of this.clients) { + if (client?.readyState === WebSocket.OPEN) { + client.send(message); + } + } + } + + public start(): void { + this.on('connection', (socket: WebSocket, request: IncomingMessage): void => { + const protocolIndex = socket.protocol.indexOf(Protocol.UI); + const version = socket.protocol.substring(protocolIndex + Protocol.UI.length) as ProtocolVersion; + this.uiService = UIServiceFactory.getUIServiceImplementation(version, this); + if (!this.uiService) { + throw new BaseError(`Could not find a UI service implementation for protocol version ${version}`); + } + // FIXME: check connection validity + socket.on('message', (messageData) => { + let [command, payload]: ProtocolRequest = [ProtocolCommand.UNKNOWN, {}]; + const protocolRequest = JSON.parse(messageData.toString()) as ProtocolRequest; + if (Utils.isIterable(protocolRequest)) { + [command, payload] = protocolRequest; + } else { + throw new BaseError('Protocol request is not iterable'); + } + this.uiService.handleMessage(command, payload).catch(() => { + logger.error(`${this.logPrefix()} Error while handling command %s message: %j`, command, payload); + }); + }); + socket.on('error', (error) => { + logger.error(`${this.logPrefix()} Error on WebSocket: %j`, error); + }); + }); + } + + public stop(): void { + this.close(); + } + + public logPrefix(): string { + return Utils.logPrefix('WebSocket Server:'); + } +} diff --git a/src/charging-station/UIWebSocketServices/AbstractUIService.ts b/src/charging-station/UIWebSocketServices/AbstractUIService.ts new file mode 100644 index 00000000..fafcdb6d --- /dev/null +++ b/src/charging-station/UIWebSocketServices/AbstractUIService.ts @@ -0,0 +1,50 @@ +import { ProtocolCommand, ProtocolRequestHandler } from '../../types/UIProtocol'; + +import BaseError from '../../exception/BaseError'; +import UIWebSocketServer from '../UIWebSocketServer'; +import logger from '../../utils/Logger'; + +export default abstract class AbstractUIService { + public readonly chargingStations: Set; + protected readonly uiWebSocketServer: UIWebSocketServer; + protected readonly messageHandlers: Map; + + constructor(uiWebSocketServer: UIWebSocketServer) { + this.chargingStations = new Set(); + this.uiWebSocketServer = uiWebSocketServer; + // TODO: Move the shared service code to AbstractUIService + this.messageHandlers = new Map([ + [ProtocolCommand.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)], + ]); + } + + public async handleMessage(command: ProtocolCommand, payload: Record): Promise { + let messageResponse: Record; + if (this.messageHandlers.has(command)) { + try { + // Call the method to build the message response + messageResponse = await this.messageHandlers.get(command)(payload) as Record; + } catch (error) { + // Log + logger.error(this.uiWebSocketServer.logPrefix() + ' Handle message error: %j', error); + throw error; + } + } else { + // Throw exception + throw new BaseError(`${command} is not implemented to handle message payload ${JSON.stringify(payload, null, 2)}`); + } + // Send the built message response + this.uiWebSocketServer.broadcastToClients(this.buildProtocolMessage(command, messageResponse)); + } + + protected buildProtocolMessage( + command: ProtocolCommand, + payload: Record, + ): string { + return JSON.stringify([command, payload]); + } + + protected handleListChargingStations(): Set { + return this.chargingStations; + } +} diff --git a/src/charging-station/UIWebSocketServices/UIServiceFactory.ts b/src/charging-station/UIWebSocketServices/UIServiceFactory.ts new file mode 100644 index 00000000..a4d8d6e8 --- /dev/null +++ b/src/charging-station/UIWebSocketServices/UIServiceFactory.ts @@ -0,0 +1,19 @@ +import AbstractUIService from './AbstractUIService'; +import { ProtocolVersion } from '../../types/UIProtocol'; +import UIService_0_0_1 from './UIService_0_0_1'; +import UIWebSocketServer from '../UIWebSocketServer'; + +export default class UIServiceFactory { + private constructor() { + // This is intentional + } + + public static getUIServiceImplementation(version: ProtocolVersion, uiWebSocketServer: UIWebSocketServer): AbstractUIService | null { + switch (version) { + case ProtocolVersion['0.0.1']: + return new UIService_0_0_1(uiWebSocketServer); + default: + return null; + } + } +} diff --git a/src/charging-station/UIWebSocketServices/UIServiceUtils.ts b/src/charging-station/UIWebSocketServices/UIServiceUtils.ts new file mode 100644 index 00000000..f95a7485 --- /dev/null +++ b/src/charging-station/UIWebSocketServices/UIServiceUtils.ts @@ -0,0 +1,23 @@ +import { Protocol, ProtocolVersion } from '../../types/UIProtocol'; + +import { IncomingMessage } from 'http'; +import Utils from '../../utils/Utils'; +import logger from '../../utils/Logger'; + +export class UIServiceUtils { + public static handleProtocols = (protocols: Set, request: IncomingMessage): string | false => { + let protocolIndex: number; + let protocol: Protocol; + let version: ProtocolVersion; + for (const fullProtocol of protocols) { + protocolIndex = fullProtocol.indexOf(Protocol.UI); + protocol = fullProtocol.substring(protocolIndex, protocolIndex + Protocol.UI.length) as Protocol; + version = fullProtocol.substring(protocolIndex + Protocol.UI.length) as ProtocolVersion; + if (Object.values(Protocol).includes(protocol) && Object.values(ProtocolVersion).includes(version)) { + return fullProtocol; + } + } + logger.error(`${Utils.logPrefix('WebSocket Server:')} Unsupported protocol: ${protocol} or protocol version: ${version}`); + return false; + }; +} diff --git a/src/charging-station/UIWebSocketServices/UIService_0_0_1.ts b/src/charging-station/UIWebSocketServices/UIService_0_0_1.ts new file mode 100644 index 00000000..eebaf011 --- /dev/null +++ b/src/charging-station/UIWebSocketServices/UIService_0_0_1.ts @@ -0,0 +1,14 @@ +import AbstractUIService from './AbstractUIService'; +import { ProtocolCommand } from '../../types/UIProtocol'; +import UIWebSocketServer from '../UIWebSocketServer'; + +export default class UIService_0_0_1 extends AbstractUIService { + constructor(uiWebSocketServer: UIWebSocketServer) { + super(uiWebSocketServer); + this.messageHandlers.set(ProtocolCommand.START_TRANSACTION, this.handleStartTransaction.bind(this)); + this.messageHandlers.set(ProtocolCommand.STOP_TRANSACTION, this.handleStopTransaction.bind(this)); + } + + private handleStartTransaction(payload: Record): void { } + private handleStopTransaction(payload: Record): void { } +} diff --git a/src/charging-station/WebSocketServer.ts b/src/charging-station/WebSocketServer.ts deleted file mode 100644 index e36725ca..00000000 --- a/src/charging-station/WebSocketServer.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { ProtocolCommand, ProtocolRequest, ProtocolVersion } from '../types/UIProtocol'; - -import AbstractUIService from './WebSocketServices/ui/AbstractUIService'; -import { IncomingMessage } from 'http'; -import UIService from './WebSocketServices/ui/0.0.1/UIService'; -import Utils from '../utils/Utils'; -import WebSocket from 'ws'; -import logger from '../utils/Logger'; - -export default class WebSocketServer extends WebSocket.Server { - public webSocketServerService: AbstractUIService; - - public constructor(options?: WebSocket.ServerOptions, callback?: () => void) { - // Create the WebSocket Server - super(options ?? { port: 80 }, callback); - // FIXME: version the instantiation - this.webSocketServerService = new UIService(this); - } - - public broadcastToClients(message: string | Record): void { - for (const client of this.clients) { - if (client?.readyState === WebSocket.OPEN) { - client.send(message); - } - } - } - - public start(): void { - this.on('connection', (socket: WebSocket, request: IncomingMessage): void => { - // FIXME: check connection validity - socket.on('message', (messageData) => { - let [version, command, payload]: ProtocolRequest = [ProtocolVersion['0.0.1'], ProtocolCommand.UNKNOWN, {}]; - // FIXME: check for iterable object - [version, command, payload] = JSON.parse(messageData.toString()) as ProtocolRequest; - switch (version) { - case ProtocolVersion['0.0.1']: - this.webSocketServerService.handleMessage(version, command, payload).catch(() => { - logger.error(`${this.logPrefix()} Error while handling command %s message: %j`, command, payload); - }); - break; - default: - logger.error(`${this.logPrefix()} Unknown protocol version: ${version}`); - } - }); - socket.on('error', (error) => { - logger.error(`${this.logPrefix()} Error on WebSocket: %j`, error); - }); - }); - } - - public stop(): void { - this.close(); - } - - public logPrefix(): string { - return Utils.logPrefix('WebSocket Server:'); - } -} diff --git a/src/charging-station/WebSocketServices/ui/0.0.1/UIService.ts b/src/charging-station/WebSocketServices/ui/0.0.1/UIService.ts deleted file mode 100644 index 6c507fd8..00000000 --- a/src/charging-station/WebSocketServices/ui/0.0.1/UIService.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ProtocolCommand, ProtocolRequestHandler, ProtocolVersion } from '../../../../types/UIProtocol'; - -import AbstractUIService from '../AbstractUIService'; -import BaseError from '../../../../exception/BaseError'; -import WebSocketServer from '../../../WebSocketServer'; -import logger from '../../../../utils/Logger'; - -export default class UIService extends AbstractUIService { - private readonly messageHandlers: Map; - - constructor(webSocketServer: WebSocketServer) { - super(webSocketServer); - this.messageHandlers = new Map([ - [ProtocolCommand.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)], - [ProtocolCommand.START_TRANSACTION, this.handleStartTransaction.bind(this)], - [ProtocolCommand.STOP_TRANSACTION, this.handleStopTransaction.bind(this)], - ]); - } - - async handleMessage(version: ProtocolVersion, command: ProtocolCommand, payload: Record): Promise { - let messageResponse: Record; - if (this.messageHandlers.has(command)) { - try { - // Call the method to build the response - messageResponse = await this.messageHandlers.get(command)(payload); - } catch (error) { - // Log - logger.error(this.webSocketServer.logPrefix() + ' Handle message error: %j', error); - throw error; - } - } else { - // Throw exception - throw new BaseError(`${command} is not implemented to handle message payload ${JSON.stringify(payload, null, 2)}`); - } - // Send the built response - this.webSocketServer.broadcastToClients(this.buildProtocolMessage(version, command, messageResponse)); - } - - private handleListChargingStations(payload: Record) { - return this.chargingStations; - } - - private handleStartTransaction(payload: Record) { } - private handleStopTransaction(payload: Record) { } -} diff --git a/src/charging-station/WebSocketServices/ui/AbstractUIService.ts b/src/charging-station/WebSocketServices/ui/AbstractUIService.ts deleted file mode 100644 index d8c61861..00000000 --- a/src/charging-station/WebSocketServices/ui/AbstractUIService.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ProtocolCommand, ProtocolVersion } from '../../../types/UIProtocol'; - -import WebSocketServer from '../../WebSocketServer'; - -export default abstract class AbstractUIService { - public readonly chargingStations: Set; - protected readonly webSocketServer: WebSocketServer; - - constructor(webSocketServer: WebSocketServer) { - this.chargingStations = new Set(); - this.webSocketServer = webSocketServer; - } - - protected buildProtocolMessage( - version: ProtocolVersion, - command: ProtocolCommand, - payload: Record, - ): string { - return JSON.stringify([version, command, payload]); - } - - abstract handleMessage(version: ProtocolVersion, command: ProtocolCommand, payload: Record): Promise; -} diff --git a/src/types/UIProtocol.ts b/src/types/UIProtocol.ts index 3e4d6232..2abefe25 100644 --- a/src/types/UIProtocol.ts +++ b/src/types/UIProtocol.ts @@ -1,3 +1,6 @@ +export enum Protocol { + UI = 'ui', +} export enum ProtocolVersion { '0.0.1' = '0.0.1', @@ -11,6 +14,6 @@ export enum ProtocolCommand { UNKNOWN = 'unknown', } -export type ProtocolRequest = [ProtocolVersion, ProtocolCommand, Record]; +export type ProtocolRequest = [ProtocolCommand, Record]; -export type ProtocolRequestHandler = (payload: Record) => Record | Promise>; +export type ProtocolRequestHandler = (payload: Record) => void | Promise | Record | Promise>;