import { ApplicationError } from '@theia/core/lib/common/application-error'; import { Disposable } from '@theia/core/lib/common/disposable'; import { Emitter } from '@theia/core/lib/common/event'; import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageType } from '@theia/core/lib/common/message-service-protocol'; import { nls } from '@theia/core/lib/common/nls'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { inject, injectable } from '@theia/core/shared/inversify'; import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager'; import { BoardIdentifier, PortIdentifier } from '../common/protocol'; import { BoardListItem, boardListItemEquals, getInferredBoardOrBoard, } from '../common/protocol/board-list'; import { Monitor, MonitorManagerProxyClient, MonitorManagerProxyFactory, MonitorSettings, PluggableMonitorSettings, } from '../common/protocol/monitor-service'; import { BoardsServiceProvider } from './boards/boards-service-provider'; @injectable() export class MonitorManagerProxyClientImpl implements MonitorManagerProxyClient { @inject(MessageService) private readonly messageService: MessageService; // This is necessary to call the backend methods from the frontend @inject(MonitorManagerProxyFactory) private readonly server: MonitorManagerProxyFactory; @inject(BoardsServiceProvider) private readonly boardsServiceProvider: BoardsServiceProvider; @inject(NotificationManager) private readonly notificationManager: NotificationManager; // When pluggable monitor messages are received from the backend // this event is triggered. // Ideally a frontend component is connected to this event // to update the UI. private readonly onMessagesReceivedEmitter = new Emitter<{ messages: string[]; }>(); readonly onMessagesReceived = this.onMessagesReceivedEmitter.event; private readonly onMonitorSettingsDidChangeEmitter = new Emitter(); readonly onMonitorSettingsDidChange = this.onMonitorSettingsDidChangeEmitter.event; private readonly onMonitorShouldResetEmitter = new Emitter(); readonly onMonitorShouldReset = this.onMonitorShouldResetEmitter.event; // WebSocket used to handle pluggable monitor communication between // frontend and backend. private webSocket?: WebSocket; private wsPort?: number; private lastConnectedBoard: BoardListItem | undefined; private onBoardListDidChange: Disposable | undefined; getWebSocketPort(): number | undefined { return this.wsPort; } /** * Connects a localhost WebSocket using the specified port. * @param addressPort port of the WebSocket */ async connect(addressPort: number): Promise { if (this.webSocket) { if (this.wsPort === addressPort) { return; } this.disconnect(); } try { this.webSocket = new WebSocket(`ws://localhost:${addressPort}`); } catch { this.messageService.error( nls.localize( 'arduino/monitor/unableToConnectToWebSocket', 'Unable to connect to websocket' ) ); return; } const opened = new Deferred(); this.webSocket.onopen = () => opened.resolve(); this.webSocket.onerror = () => opened.reject(); this.webSocket.onmessage = (message) => { const parsedMessage = JSON.parse(message.data); if (Array.isArray(parsedMessage)) this.onMessagesReceivedEmitter.fire({ messages: parsedMessage }); else if ( parsedMessage.command === Monitor.MiddlewareCommand.ON_SETTINGS_DID_CHANGE ) { this.onMonitorSettingsDidChangeEmitter.fire(parsedMessage.data); } }; this.wsPort = addressPort; return opened.promise; } /** * Disconnects the WebSocket if connected. */ disconnect(): void { if (!this.webSocket) { return; } this.onBoardListDidChange?.dispose(); this.onBoardListDidChange = undefined; try { this.webSocket.close(); this.webSocket = undefined; } catch (err) { console.error( 'Could not close the websocket connection for the monitor.', err ); this.messageService.error( nls.localize( 'arduino/monitor/unableToCloseWebSocket', 'Unable to close websocket' ) ); } } async isWSConnected(): Promise { return !!this.webSocket; } async startMonitor(settings?: PluggableMonitorSettings): Promise { const { boardList } = this.boardsServiceProvider; this.lastConnectedBoard = boardList.items[boardList.selectedIndex]; if (!this.onBoardListDidChange) { this.onBoardListDidChange = this.boardsServiceProvider.onBoardListDidChange( async (newBoardList) => { const currentConnectedBoard = newBoardList.items[newBoardList.selectedIndex]; if (!currentConnectedBoard) { return; } if ( !this.lastConnectedBoard || boardListItemEquals( currentConnectedBoard, this.lastConnectedBoard ) ) { // a board is plugged and it's the same as prev, rerun "this.startMonitor" to // recreate the listener callback this.startMonitor(); } else { // a board is plugged and it's different from the old connected board this.lastConnectedBoard = currentConnectedBoard; this.onMonitorShouldResetEmitter.fire(); } } ); } if (!this.lastConnectedBoard) { return; } const board = getInferredBoardOrBoard(this.lastConnectedBoard); if (!board) { return; } try { this.clearVisibleNotification(); await this.server().startMonitor( board, this.lastConnectedBoard.port, settings ); } catch (err) { const message = ApplicationError.is(err) ? err.message : String(err); this.previousNotificationId = this.notificationId(message); this.messageService.error(message); } } getCurrentSettings( board: BoardIdentifier, port: PortIdentifier ): Promise { return this.server().getCurrentSettings(board, port); } send(message: string): void { if (!this.webSocket) { return; } this.webSocket.send( JSON.stringify({ command: Monitor.ClientCommand.SEND_MESSAGE, data: message, }) ); } changeSettings(settings: MonitorSettings): void { if (!this.webSocket) { return; } this.webSocket.send( JSON.stringify({ command: Monitor.ClientCommand.CHANGE_SETTINGS, data: settings, }) ); } /** * This is the internal (Theia) ID of the notification that is currently visible. * It's stored here as a field to be able to close it before starting a new monitor connection. It's a hack. */ private previousNotificationId: string | undefined; private clearVisibleNotification(): void { if (this.previousNotificationId) { this.notificationManager.clear(this.previousNotificationId); this.previousNotificationId = undefined; } } private notificationId(message: string, ...actions: string[]): string { return this.notificationManager['getMessageId']({ text: message, actions, type: MessageType.Error, }); } }