diff --git a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts index 3666238a..2a7b7cc9 100644 --- a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts +++ b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts @@ -1,103 +1,123 @@ -import { Emitter, MessageService } from "@theia/core"; -import { inject, injectable } from "@theia/core/shared/inversify"; -import { Board, Port } from "../common/protocol"; -import { Monitor, MonitorManagerProxyClient, MonitorManagerProxyFactory, MonitorSettings } from "../common/protocol/monitor-service"; +import { Emitter, MessageService } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { Board, Port } from '../common/protocol'; +import { + Monitor, + MonitorManagerProxyClient, + MonitorManagerProxyFactory, + MonitorSettings, +} from '../common/protocol/monitor-service'; @injectable() -export class MonitorManagerProxyClientImpl implements MonitorManagerProxyClient { - // 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. - protected readonly onMessagesReceivedEmitter = new Emitter<{ messages: string[] }>(); - readonly onMessagesReceived = this.onMessagesReceivedEmitter.event; +export class MonitorManagerProxyClientImpl + implements MonitorManagerProxyClient +{ + // 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. + protected readonly onMessagesReceivedEmitter = new Emitter<{ + messages: string[]; + }>(); + readonly onMessagesReceived = this.onMessagesReceivedEmitter.event; - // WebSocket used to handle pluggable monitor communication between - // frontend and backend. - private webSocket?: WebSocket; - private wsPort?: number; + protected readonly onWSConnectionChangedEmitter = new Emitter(); + readonly onWSConnectionChanged = this.onWSConnectionChangedEmitter.event; - getWebSocketPort(): number | undefined { - return this.wsPort; + // WebSocket used to handle pluggable monitor communication between + // frontend and backend. + private webSocket?: WebSocket; + private wsPort?: number; + + getWebSocketPort(): number | undefined { + return this.wsPort; + } + + constructor( + @inject(MessageService) + protected messageService: MessageService, + + // This is necessary to call the backend methods from the frontend + @inject(MonitorManagerProxyFactory) + protected server: MonitorManagerProxyFactory + ) {} + + /** + * Connects a localhost WebSocket using the specified port. + * @param addressPort port of the WebSocket + */ + connect(addressPort: number): void { + if (this.webSocket) { + return; + } + try { + this.webSocket = new WebSocket(`ws://localhost:${addressPort}`); + this.onWSConnectionChangedEmitter.fire(true); + } catch { + this.messageService.error('Unable to connect to websocket'); + return; } - constructor( - @inject(MessageService) - protected messageService: MessageService, + this.webSocket.onmessage = (res) => { + const messages = JSON.parse(res.data); + this.onMessagesReceivedEmitter.fire({ messages }); + }; + this.wsPort = addressPort; + } - // This is necessary to call the backend methods from the frontend - @inject(MonitorManagerProxyFactory) - protected server: MonitorManagerProxyFactory - ) { + /** + * Disconnects the WebSocket if connected. + */ + disconnect(): void { + try { + this.webSocket?.close(); + this.webSocket = undefined; + this.onWSConnectionChangedEmitter.fire(false); + } catch { + this.messageService.error('Unable to close websocket'); + } + } + async isWSConnected(): Promise { + return !!this.webSocket; + } + + async startMonitor( + board: Board, + port: Port, + settings?: MonitorSettings + ): Promise { + return this.server().startMonitor(board, port, settings); + } + + getCurrentSettings(board: Board, port: Port): MonitorSettings { + return this.server().getCurrentSettings(board, port); + } + + send(message: string): void { + if (!this.webSocket) { + return; } - /** - * Connects a localhost WebSocket using the specified port. - * @param addressPort port of the WebSocket - */ - connect(addressPort: number): void { - if (this.webSocket) { - return; - } - try { - this.webSocket = new WebSocket(`ws://localhost:${addressPort}`); - } catch { - this.messageService.error('Unable to connect to websocket'); - return; - } + this.webSocket.send( + JSON.stringify({ + command: Monitor.Command.SEND_MESSAGE, + data: message, + }) + ); + } - this.webSocket.onmessage = (res) => { - const messages = JSON.parse(res.data); - this.onMessagesReceivedEmitter.fire({ messages }); - } - this.wsPort = addressPort; + changeSettings(settings: MonitorSettings): void { + if (!this.webSocket) { + return; } - /** - * Disconnects the WebSocket if connected. - */ - disconnect(): void { - try { - this.webSocket?.close(); - this.webSocket = undefined; - } catch { - this.messageService.error('Unable to close websocket'); - } - } - - async isWSConnected(): Promise { - return !!this.webSocket; - } - - async startMonitor(board: Board, port: Port, settings?: MonitorSettings): Promise { - return this.server().startMonitor(board, port, settings); - } - - getCurrentSettings(board: Board, port: Port): MonitorSettings { - return this.server().getCurrentSettings(board, port); - } - - send(message: string): void { - if (!this.webSocket) { - return; - } - - this.webSocket.send(JSON.stringify({ - command: Monitor.Command.SEND_MESSAGE, - data: message, - })); - } - - changeSettings(settings: MonitorSettings): void { - if (!this.webSocket) { - return; - } - - this.webSocket.send(JSON.stringify({ - command: Monitor.Command.CHANGE_SETTINGS, - // TODO: This might be wrong, verify if it works - data: settings, - })); - } + this.webSocket.send( + JSON.stringify({ + command: Monitor.Command.CHANGE_SETTINGS, + // TODO: This might be wrong, verify if it works + data: settings, + }) + ); + } } diff --git a/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx b/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx index ed545fc8..59ad9c9a 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx @@ -5,6 +5,7 @@ import { isOSX } from '@theia/core/lib/common/os'; import { DisposableCollection, nls } from '@theia/core/lib/common'; import { MonitorManagerProxyClient } from '../../../common/protocol'; import { BoardsServiceProvider } from '../../boards/boards-service-provider'; +import { timeout } from '@theia/core/lib/common/promise-util'; export namespace SerialMonitorSendInput { export interface Props { @@ -27,16 +28,33 @@ export class SerialMonitorSendInput extends React.Component< constructor(props: Readonly) { super(props); - this.state = { text: '', connected: false }; + this.state = { text: '', connected: true }; this.onChange = this.onChange.bind(this); this.onSend = this.onSend.bind(this); this.onKeyDown = this.onKeyDown.bind(this); } componentDidMount(): void { - this.props.monitorManagerProxy.isWSConnected().then((connected) => { - this.setState({ connected }); + this.setState({ connected: true }); + + const checkWSConnection = new Promise((resolve) => { + this.props.monitorManagerProxy.onWSConnectionChanged((connected) => { + this.setState({ connected }); + resolve(true); + }); }); + + const checkWSTimeout = timeout(1000).then(() => false); + + Promise.race([checkWSConnection, checkWSTimeout]).then( + async (resolved) => { + if (!resolved) { + const connected = + await this.props.monitorManagerProxy.isWSConnected(); + this.setState({ connected }); + } + } + ); } componentWillUnmount(): void { @@ -49,7 +67,7 @@ export class SerialMonitorSendInput extends React.Component< MonitorManagerProxy; export const MonitorManagerProxyPath = '/services/monitor-manager-proxy'; export const MonitorManagerProxy = Symbol('MonitorManagerProxy'); -export interface MonitorManagerProxy extends JsonRpcServer { - startMonitor(board: Board, port: Port, settings?: MonitorSettings): Promise; - changeMonitorSettings(board: Board, port: Port, settings: MonitorSettings): Promise; - stopMonitor(board: Board, port: Port): Promise; - getCurrentSettings(board: Board, port: Port): MonitorSettings; +export interface MonitorManagerProxy + extends JsonRpcServer { + startMonitor( + board: Board, + port: Port, + settings?: MonitorSettings + ): Promise; + changeMonitorSettings( + board: Board, + port: Port, + settings: MonitorSettings + ): Promise; + stopMonitor(board: Board, port: Port): Promise; + getCurrentSettings(board: Board, port: Port): MonitorSettings; } export const MonitorManagerProxyClient = Symbol('MonitorManagerProxyClient'); export interface MonitorManagerProxyClient { - onMessagesReceived: Event<{ messages: string[] }>; - connect(addressPort: number): void; - disconnect(): void; - getWebSocketPort(): number | undefined; - isWSConnected(): Promise; - startMonitor(board: Board, port: Port, settings?: MonitorSettings): Promise; - getCurrentSettings(board: Board, port: Port): MonitorSettings; - send(message: string): void; - changeSettings(settings: MonitorSettings): void + onMessagesReceived: Event<{ messages: string[] }>; + onWSConnectionChanged: Event; + connect(addressPort: number): void; + disconnect(): void; + getWebSocketPort(): number | undefined; + isWSConnected(): Promise; + startMonitor( + board: Board, + port: Port, + settings?: MonitorSettings + ): Promise; + getCurrentSettings(board: Board, port: Port): MonitorSettings; + send(message: string): void; + changeSettings(settings: MonitorSettings): void; } export interface MonitorSetting { - // The setting identifier - readonly id: string; - // A human-readable label of the setting (to be displayed on the GUI) - readonly label: string; - // The setting type (at the moment only "enum" is avaiable) - readonly type: string; - // The values allowed on "enum" types - readonly values: string[]; - // The selected value - selectedValue: string; + // The setting identifier + readonly id: string; + // A human-readable label of the setting (to be displayed on the GUI) + readonly label: string; + // The setting type (at the moment only "enum" is avaiable) + readonly type: string; + // The values allowed on "enum" types + readonly values: string[]; + // The selected value + selectedValue: string; } export type MonitorSettings = Record; export namespace Monitor { - export enum Command { - SEND_MESSAGE = 'MONITOR_SEND_MESSAGE', - CHANGE_SETTINGS = 'MONITOR_CHANGE_SETTINGS', - } + export enum Command { + SEND_MESSAGE = 'MONITOR_SEND_MESSAGE', + CHANGE_SETTINGS = 'MONITOR_CHANGE_SETTINGS', + } - export type Message = { - command: Monitor.Command, - data: string; - } + export type Message = { + command: Monitor.Command; + data: string; + }; } -export interface Status { } +export interface Status {} export type OK = Status; export interface ErrorStatus extends Status { - readonly message: string; + readonly message: string; } export namespace Status { - export function isOK(status: Status & { message?: string }): status is OK { - return !!status && typeof status.message !== 'string'; - } - export const OK: OK = {}; - export const NOT_CONNECTED: ErrorStatus = { message: 'Not connected.' }; - export const ALREADY_CONNECTED: ErrorStatus = { - message: 'Already connected.', - }; - export const CONFIG_MISSING: ErrorStatus = { - message: 'Serial Config missing.', - }; + export function isOK(status: Status & { message?: string }): status is OK { + return !!status && typeof status.message !== 'string'; + } + export const OK: OK = {}; + export const NOT_CONNECTED: ErrorStatus = { message: 'Not connected.' }; + export const ALREADY_CONNECTED: ErrorStatus = { + message: 'Already connected.', + }; + export const CONFIG_MISSING: ErrorStatus = { + message: 'Serial Config missing.', + }; } diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index a51c4003..fa8b708f 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -1,8 +1,8 @@ -import { ILogger } from "@theia/core"; -import { inject, injectable, named } from "@theia/core/shared/inversify"; -import { Board, Port, Status, MonitorSettings } from "../common/protocol"; -import { CoreClientAware } from "./core-client-provider"; -import { MonitorService } from "./monitor-service"; +import { ILogger } from '@theia/core'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { Board, Port, Status, MonitorSettings } from '../common/protocol'; +import { CoreClientAware } from './core-client-provider'; +import { MonitorService } from './monitor-service'; type MonitorID = string; @@ -10,191 +10,194 @@ export const MonitorManagerName = 'monitor-manager'; @injectable() export class MonitorManager extends CoreClientAware { - // Map of monitor services that manage the running pluggable monitors. - // Each service handles the lifetime of one, and only one, monitor. - // If either the board or port managed changes a new service must - // be started. - private monitorServices = new Map(); + // Map of monitor services that manage the running pluggable monitors. + // Each service handles the lifetime of one, and only one, monitor. + // If either the board or port managed changes a new service must + // be started. + private monitorServices = new Map(); - constructor( - @inject(ILogger) - @named(MonitorManagerName) - protected readonly logger: ILogger, - ) { - super(); - } + constructor( + @inject(ILogger) + @named(MonitorManagerName) + protected readonly logger: ILogger + ) { + super(); + } - /** - * Used to know if a monitor is started - * @param board board connected to port - * @param port port to monitor - * @returns true if the monitor is currently monitoring the board/port - * combination specifed, false in all other cases. - */ - isStarted(board: Board, port: Port): boolean { - const monitorID = this.monitorID(board, port); - const monitor = this.monitorServices.get(monitorID); - if (monitor) { - return monitor.isStarted(); - } - return false; + /** + * Used to know if a monitor is started + * @param board board connected to port + * @param port port to monitor + * @returns true if the monitor is currently monitoring the board/port + * combination specifed, false in all other cases. + */ + isStarted(board: Board, port: Port): boolean { + const monitorID = this.monitorID(board, port); + const monitor = this.monitorServices.get(monitorID); + if (monitor) { + return monitor.isStarted(); } + return false; + } - /** - * Start a pluggable monitor that receives and sends messages - * to the specified board and port combination. - * @param board board connected to port - * @param port port to monitor - * @returns a Status object to know if the process has been - * started or if there have been errors. - */ - async startMonitor(board: Board, port: Port): Promise { - const monitorID = this.monitorID(board, port); - let monitor = this.monitorServices.get(monitorID); - if (!monitor) { - monitor = this.createMonitor(board, port) - } - return await monitor.start(); + /** + * Start a pluggable monitor that receives and sends messages + * to the specified board and port combination. + * @param board board connected to port + * @param port port to monitor + * @returns a Status object to know if the process has been + * started or if there have been errors. + */ + async startMonitor(board: Board, port: Port): Promise { + const monitorID = this.monitorID(board, port); + let monitor = this.monitorServices.get(monitorID); + if (!monitor) { + monitor = this.createMonitor(board, port); } + return await monitor.start(); + } - /** - * Stop a pluggable monitor connected to the specified board/port - * combination. It's a noop if monitor is not running. - * @param board board connected to port - * @param port port monitored - */ - async stopMonitor(board: Board, port: Port): Promise { - const monitorID = this.monitorID(board, port); - const monitor = this.monitorServices.get(monitorID); - if (!monitor) { - // There's no monitor to stop, bail - return; - } - return await monitor.stop(); + /** + * Stop a pluggable monitor connected to the specified board/port + * combination. It's a noop if monitor is not running. + * @param board board connected to port + * @param port port monitored + */ + async stopMonitor(board: Board, port: Port): Promise { + const monitorID = this.monitorID(board, port); + const monitor = this.monitorServices.get(monitorID); + if (!monitor) { + // There's no monitor to stop, bail + return; } + return await monitor.stop(); + } - /** - * Returns the port of the WebSocket used by the MonitorService - * that is handling the board/port combination - * @param board board connected to port - * @param port port to monitor - * @returns port of the MonitorService's WebSocket - */ - getWebsocketAddressPort(board: Board, port: Port): number { - const monitorID = this.monitorID(board, port); - const monitor = this.monitorServices.get(monitorID); - if (!monitor) { - return -1; - } - return monitor.getWebsocketAddressPort(); + /** + * Returns the port of the WebSocket used by the MonitorService + * that is handling the board/port combination + * @param board board connected to port + * @param port port to monitor + * @returns port of the MonitorService's WebSocket + */ + getWebsocketAddressPort(board: Board, port: Port): number { + const monitorID = this.monitorID(board, port); + const monitor = this.monitorServices.get(monitorID); + if (!monitor) { + return -1; } + return monitor.getWebsocketAddressPort(); + } - /** - * Notifies the monitor service of that board/port combination - * that an upload process started on that exact board/port combination. - * This must be done so that we can stop the monitor for the time being - * until the upload process finished. - * @param board board connected to port - * @param port port to monitor - */ - async notifyUploadStarted(board?: Board, port?: Port): Promise { - if (!board || !port) { - // We have no way of knowing which monitor - // to retrieve if we don't have this information. - return; - } - const monitorID = this.monitorID(board, port); - const monitor = this.monitorServices.get(monitorID); - if (!monitor) { - // There's no monitor running there, bail - return; - } - return await monitor.pause(); + /** + * Notifies the monitor service of that board/port combination + * that an upload process started on that exact board/port combination. + * This must be done so that we can stop the monitor for the time being + * until the upload process finished. + * @param board board connected to port + * @param port port to monitor + */ + async notifyUploadStarted(board?: Board, port?: Port): Promise { + if (!board || !port) { + // We have no way of knowing which monitor + // to retrieve if we don't have this information. + return; } + const monitorID = this.monitorID(board, port); + const monitor = this.monitorServices.get(monitorID); + if (!monitor) { + // There's no monitor running there, bail + return; + } + return await monitor.pause(); + } - /** - * Notifies the monitor service of that board/port combination - * that an upload process started on that exact board/port combination. - * @param board board connected to port - * @param port port to monitor - * @returns a Status object to know if the process has been - * started or if there have been errors. - */ - async notifyUploadFinished(board?: Board, port?: Port): Promise { - if (!board || !port) { - // We have no way of knowing which monitor - // to retrieve if we don't have this information. - return Status.NOT_CONNECTED; - } - const monitorID = this.monitorID(board, port); - const monitor = this.monitorServices.get(monitorID); - if (!monitor) { - // There's no monitor running there, bail - return Status.NOT_CONNECTED; - } - return await monitor.start(); + /** + * Notifies the monitor service of that board/port combination + * that an upload process started on that exact board/port combination. + * @param board board connected to port + * @param port port to monitor + * @returns a Status object to know if the process has been + * started or if there have been errors. + */ + async notifyUploadFinished(board?: Board, port?: Port): Promise { + if (!board || !port) { + // We have no way of knowing which monitor + // to retrieve if we don't have this information. + return Status.NOT_CONNECTED; } + const monitorID = this.monitorID(board, port); + const monitor = this.monitorServices.get(monitorID); + if (!monitor) { + // There's no monitor running there, bail + return Status.NOT_CONNECTED; + } + return await monitor.start(); + } - /** - * Changes the settings of a pluggable monitor even if it's running. - * If monitor is not running they're going to be used as soon as it's started. - * @param board board connected to port - * @param port port to monitor - * @param settings monitor settings to change - */ - changeMonitorSettings(board: Board, port: Port, settings: MonitorSettings) { - const monitorID = this.monitorID(board, port); - let monitor = this.monitorServices.get(monitorID); - if (!monitor) { - monitor = this.createMonitor(board, port) - monitor.changeSettings(settings); - } + /** + * Changes the settings of a pluggable monitor even if it's running. + * If monitor is not running they're going to be used as soon as it's started. + * @param board board connected to port + * @param port port to monitor + * @param settings monitor settings to change + */ + changeMonitorSettings(board: Board, port: Port, settings: MonitorSettings) { + const monitorID = this.monitorID(board, port); + let monitor = this.monitorServices.get(monitorID); + if (!monitor) { + monitor = this.createMonitor(board, port); + monitor.changeSettings(settings); } + } - /** - * Returns the settings currently used by the pluggable monitor - * that's communicating with the specified board/port combination. - * @param board board connected to port - * @param port port monitored - * @returns map of current monitor settings - */ - currentMonitorSettings(board: Board, port: Port): MonitorSettings { - const monitorID = this.monitorID(board, port); - const monitor = this.monitorServices.get(monitorID); - if (!monitor) { - return {}; - } - return monitor.currentSettings(); + /** + * Returns the settings currently used by the pluggable monitor + * that's communicating with the specified board/port combination. + * @param board board connected to port + * @param port port monitored + * @returns map of current monitor settings + */ + currentMonitorSettings(board: Board, port: Port): MonitorSettings { + const monitorID = this.monitorID(board, port); + const monitor = this.monitorServices.get(monitorID); + if (!monitor) { + return {}; } + return monitor.currentSettings(); + } - /** - * Creates a MonitorService that handles the lifetime and the - * communication via WebSocket with the frontend. - * @param board board connected to specified port - * @param port port to monitor - * @returns a new instance of MonitorService ready to use. - */ - private createMonitor(board: Board, port: Port): MonitorService { - const monitorID = this.monitorID(board, port); - const monitor = new MonitorService( - this.logger, - board, - port, - this.coreClientProvider, - ); - monitor.onDispose((() => { - this.monitorServices.delete(monitorID); - }).bind(this)); - return monitor - } + /** + * Creates a MonitorService that handles the lifetime and the + * communication via WebSocket with the frontend. + * @param board board connected to specified port + * @param port port to monitor + * @returns a new instance of MonitorService ready to use. + */ + private createMonitor(board: Board, port: Port): MonitorService { + const monitorID = this.monitorID(board, port); + const monitor = new MonitorService( + this.logger, + board, + port, + this.coreClientProvider + ); + this.monitorServices.set(monitorID, monitor); + monitor.onDispose( + (() => { + this.monitorServices.delete(monitorID); + }).bind(this) + ); + return monitor; + } - /** - * Utility function to create a unique ID for a monitor service. - * @param board - * @param port - * @returns a unique monitor ID - */ - private monitorID(board: Board, port: Port): MonitorID { - return `${board.fqbn}-${port.address}-${port.protocol}`; - } -} \ No newline at end of file + /** + * Utility function to create a unique ID for a monitor service. + * @param board + * @param port + * @returns a unique monitor ID + */ + private monitorID(board: Board, port: Port): MonitorID { + return `${board.fqbn}-${port.address}-${port.protocol}`; + } +} diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index 8d221c6a..35f77f45 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -1,365 +1,398 @@ -import { ClientDuplexStream } from "@grpc/grpc-js"; -import { Disposable, Emitter, ILogger } from "@theia/core"; -import { inject, named } from "@theia/core/shared/inversify"; -import { Board, Port, Status, MonitorSettings, Monitor } from "../common/protocol"; -import { EnumerateMonitorPortSettingsRequest, EnumerateMonitorPortSettingsResponse, MonitorPortConfiguration, MonitorPortSetting, MonitorRequest, MonitorResponse } from "./cli-protocol/cc/arduino/cli/commands/v1/monitor_pb"; -import { CoreClientAware, CoreClientProvider } from "./core-client-provider"; -import { WebSocketProvider } from "./web-socket/web-socket-provider"; -import { Port as gRPCPort } from 'arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/port_pb' -import WebSocketProviderImpl from "./web-socket/web-socket-provider-impl"; +import { ClientDuplexStream } from '@grpc/grpc-js'; +import { Disposable, Emitter, ILogger } from '@theia/core'; +import { inject, named } from '@theia/core/shared/inversify'; +import { + Board, + Port, + Status, + MonitorSettings, + Monitor, +} from '../common/protocol'; +import { + EnumerateMonitorPortSettingsRequest, + EnumerateMonitorPortSettingsResponse, + MonitorPortConfiguration, + MonitorPortSetting, + MonitorRequest, + MonitorResponse, +} from './cli-protocol/cc/arduino/cli/commands/v1/monitor_pb'; +import { CoreClientAware, CoreClientProvider } from './core-client-provider'; +import { WebSocketProvider } from './web-socket/web-socket-provider'; +import { Port as gRPCPort } from 'arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/port_pb'; +import WebSocketProviderImpl from './web-socket/web-socket-provider-impl'; export const MonitorServiceName = 'monitor-service'; export class MonitorService extends CoreClientAware implements Disposable { - // Bidirectional gRPC stream used to receive and send data from the running - // pluggable monitor managed by the Arduino CLI. - protected duplex: ClientDuplexStream | null; + // Bidirectional gRPC stream used to receive and send data from the running + // pluggable monitor managed by the Arduino CLI. + protected duplex: ClientDuplexStream | null; - // Settings used by the currently running pluggable monitor. - // They can be freely modified while running. - protected settings: MonitorSettings; + // Settings used by the currently running pluggable monitor. + // They can be freely modified while running. + protected settings: MonitorSettings; - // List of messages received from the running pluggable monitor. - // These are flushed from time to time to the frontend. - protected messages: string[] = []; + // List of messages received from the running pluggable monitor. + // These are flushed from time to time to the frontend. + protected messages: string[] = []; - // Handles messages received from the frontend via websocket. - protected onMessageReceived?: Disposable; + // Handles messages received from the frontend via websocket. + protected onMessageReceived?: Disposable; - // Sends messages to the frontend from time to time. - protected flushMessagesInterval?: NodeJS.Timeout; + // Sends messages to the frontend from time to time. + protected flushMessagesInterval?: NodeJS.Timeout; - // Triggered each time the number of clients connected - // to the this service WebSocket changes. - protected onWSClientsNumberChanged?: Disposable; + // Triggered each time the number of clients connected + // to the this service WebSocket changes. + protected onWSClientsNumberChanged?: Disposable; - // Used to notify that the monitor is being disposed - protected readonly onDisposeEmitter = new Emitter(); - readonly onDispose = this.onDisposeEmitter.event; + // Used to notify that the monitor is being disposed + protected readonly onDisposeEmitter = new Emitter(); + readonly onDispose = this.onDisposeEmitter.event; - protected readonly webSocketProvider: WebSocketProvider = new WebSocketProviderImpl(); + protected readonly webSocketProvider: WebSocketProvider = + new WebSocketProviderImpl(); - constructor( - @inject(ILogger) - @named(MonitorServiceName) - protected readonly logger: ILogger, + constructor( + @inject(ILogger) + @named(MonitorServiceName) + protected readonly logger: ILogger, - private readonly board: Board, - private readonly port: Port, - protected readonly coreClientProvider: CoreClientProvider, - ) { - super(); + private readonly board: Board, + private readonly port: Port, + protected readonly coreClientProvider: CoreClientProvider + ) { + super(); - this.onWSClientsNumberChanged = this.webSocketProvider.onClientsNumberChanged(async (clients: number) => { - if (clients === 0) { - // There are no more clients that want to receive - // data from this monitor, we can freely close - // and dispose it. - this.dispose(); - } - }); + this.onWSClientsNumberChanged = + this.webSocketProvider.onClientsNumberChanged(async (clients: number) => { + if (clients === 0) { + // There are no more clients that want to receive + // data from this monitor, we can freely close + // and dispose it. + this.dispose(); + } + }); - // Sets default settings for this monitor - this.portMonitorSettings(port.protocol, board.fqbn!).then( - settings => this.settings = settings + // Sets default settings for this monitor + this.portMonitorSettings(port.protocol, board.fqbn!).then( + (settings) => (this.settings = settings) + ); + } + + getWebsocketAddressPort(): number { + return this.webSocketProvider.getAddress().port; + } + + dispose(): void { + this.stop(); + this.onDisposeEmitter.fire(); + } + + /** + * isStarted is used to know if the currently running pluggable monitor is started. + * @returns true if pluggable monitor communication duplex is open, + * false in all other cases. + */ + isStarted(): boolean { + return !!this.duplex; + } + + /** + * Start and connects a monitor using currently set board and port. + * If a monitor is already started or board fqbn, port address and/or protocol + * are missing nothing happens. + * @returns a status to verify connection has been established. + */ + async start(): Promise { + if (this.duplex) { + return Status.ALREADY_CONNECTED; + } + + if (!this.board?.fqbn || !this.port?.address || !this.port?.protocol) { + return Status.CONFIG_MISSING; + } + + this.logger.info('starting monitor'); + await this.coreClientProvider.initialized; + const coreClient = await this.coreClient(); + const { client, instance } = coreClient; + + this.duplex = client.monitor(); + this.duplex + .on('close', () => { + this.logger.info( + `monitor to ${this.port?.address} using ${this.port?.protocol} closed by client` ); - } - - getWebsocketAddressPort(): number { - return this.webSocketProvider.getAddress().port; - } - - dispose(): void { - this.stop(); - this.onDisposeEmitter.fire(); - } - - /** - * isStarted is used to know if the currently running pluggable monitor is started. - * @returns true if pluggable monitor communication duplex is open, - * false in all other cases. - */ - isStarted(): boolean { - return !!this.duplex; - } - - /** - * Start and connects a monitor using currently set board and port. - * If a monitor is already started or board fqbn, port address and/or protocol - * are missing nothing happens. - * @returns a status to verify connection has been established. - */ - async start(): Promise { - if (this.duplex) { - return Status.ALREADY_CONNECTED; - } - - if (!this.board?.fqbn || !this.port?.address || !this.port?.protocol) { - return Status.CONFIG_MISSING - } - - this.logger.info("starting monitor"); - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); - const { client, instance } = coreClient; - - this.duplex = client.monitor() - this.duplex - .on('close', () => { - this.logger.info(`monitor to ${this.port?.address} using ${this.port?.protocol} closed by client`) - }) - .on('end', () => { - this.logger.info(`monitor to ${this.port?.address} using ${this.port?.protocol} closed by server`) - }) - .on('error', (err: Error) => { - this.logger.error(err); - // TODO - // this.theiaFEClient?.notifyError() - }) - .on('data', ((res: MonitorResponse) => { - if (res.getError()) { - // TODO: Maybe disconnect - this.logger.error(res.getError()); - return; - } - const data = res.getRxData() - const message = - typeof data === 'string' ? data : new TextDecoder('utf8').decode(data); - this.messages.push(...splitLines(message)) - }).bind(this)); - - const req = new MonitorRequest(); - req.setInstance(instance); - if (this.board?.fqbn) { - req.setFqbn(this.board.fqbn) - } - if (this.port?.address && this.port?.protocol) { - const port = new gRPCPort() - port.setAddress(this.port.address); - port.setProtocol(this.port.protocol); - req.setPort(port); - } - const config = new MonitorPortConfiguration(); - for (const id in this.settings) { - const s = new MonitorPortSetting(); - s.setSettingId(id); - s.setValue(this.settings[id].selectedValue); - config.addSettings(s); - } - req.setPortConfiguration(config) - - const connect = new Promise(resolve => { - if (this.duplex?.write(req)) { - this.startMessagesHandlers(); - this.logger.info(`started monitor to ${this.port?.address} using ${this.port?.protocol}`) - resolve(Status.OK); - return; - } - this.logger.warn(`failed starting monitor to ${this.port?.address} using ${this.port?.protocol}`) - resolve(Status.NOT_CONNECTED); - }); - - const connectTimeout = new Promise(resolve => { - setTimeout(async () => { - this.logger.warn(`timeout starting monitor to ${this.port?.address} using ${this.port?.protocol}`) - resolve(Status.NOT_CONNECTED); - }, 1000); - }); - // Try opening a monitor connection with a timeout - return await Promise.race([ - connect, - connectTimeout, - ]) - } - - /** - * Pauses the currently running monitor, it still closes the gRPC connection - * with the underlying monitor process but it doesn't stop the message handlers - * currently running. - * This is mainly used to handle upload when to the board/port combination - * the monitor is listening to. - * @returns - */ - async pause(): Promise { - return new Promise(resolve => { - if (!this.duplex) { - this.logger.warn(`monitor to ${this.port?.address} using ${this.port?.protocol} already stopped`) - return resolve(); - } - // It's enough to close the connection with the client - // to stop the monitor process - this.duplex.cancel(); - this.duplex = null; - this.logger.info(`stopped monitor to ${this.port?.address} using ${this.port?.protocol}`) - resolve(); - }) - } - - /** - * Stop the monitor currently running - */ - async stop(): Promise { - return this.pause().finally( - this.stopMessagesHandlers + }) + .on('end', () => { + this.logger.info( + `monitor to ${this.port?.address} using ${this.port?.protocol} closed by server` ); + }) + .on('error', (err: Error) => { + this.logger.error(err); + // TODO + // this.theiaFEClient?.notifyError() + }) + .on( + 'data', + ((res: MonitorResponse) => { + if (res.getError()) { + // TODO: Maybe disconnect + this.logger.error(res.getError()); + return; + } + const data = res.getRxData(); + const message = + typeof data === 'string' + ? data + : new TextDecoder('utf8').decode(data); + this.messages.push(...splitLines(message)); + }).bind(this) + ); + + const req = new MonitorRequest(); + req.setInstance(instance); + if (this.board?.fqbn) { + req.setFqbn(this.board.fqbn); } - - /** - * Send a message to the running monitor, a well behaved monitor - * will then send that message to the board. - * We MUST NEVER send a message that wasn't a user's input to the board. - * @param message string sent to running monitor - * @returns a status to verify message has been sent. - */ - async send(message: string): Promise { - if (!this.duplex) { - return Status.NOT_CONNECTED; - } - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); - const { instance } = coreClient; - - const req = new MonitorRequest(); - req.setInstance(instance); - req.setTxData(new TextEncoder().encode(message)); - return new Promise(resolve => { - if (this.duplex) { - this.duplex?.write(req, () => { - resolve(Status.OK); - }); - return; - } - this.stop().then(() => resolve(Status.NOT_CONNECTED)); - }) + if (this.port?.address && this.port?.protocol) { + const port = new gRPCPort(); + port.setAddress(this.port.address); + port.setProtocol(this.port.protocol); + req.setPort(port); } - - /** - * - * @returns map of current monitor settings - */ - currentSettings(): MonitorSettings { - return this.settings; + const config = new MonitorPortConfiguration(); + for (const id in this.settings) { + const s = new MonitorPortSetting(); + s.setSettingId(id); + s.setValue(this.settings[id].selectedValue); + config.addSettings(s); } + req.setPortConfiguration(config); - /** - * Returns the possible configurations used to connect a monitor - * to the board specified by fqbn using the specified protocol - * @param protocol the protocol of the monitor we want get settings for - * @param fqbn the fqbn of the board we want to monitor - * @returns a map of all the settings supported by the monitor - */ - private async portMonitorSettings(protocol: string, fqbn: string): Promise { - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); - const { client, instance } = coreClient; - const req = new EnumerateMonitorPortSettingsRequest(); - req.setInstance(instance); - req.setPortProtocol(protocol); - req.setFqbn(fqbn); + const connect = new Promise((resolve) => { + if (this.duplex?.write(req)) { + this.startMessagesHandlers(); + this.logger.info( + `started monitor to ${this.port?.address} using ${this.port?.protocol}` + ); + resolve(Status.OK); + return; + } + this.logger.warn( + `failed starting monitor to ${this.port?.address} using ${this.port?.protocol}` + ); + resolve(Status.NOT_CONNECTED); + }); - const res = await new Promise((resolve, reject) => { - client.enumerateMonitorPortSettings(req, (err, resp) => { - if (!!err) { - reject(err) - } - resolve(resp) - }) + const connectTimeout = new Promise((resolve) => { + setTimeout(async () => { + this.logger.warn( + `timeout starting monitor to ${this.port?.address} using ${this.port?.protocol}` + ); + resolve(Status.NOT_CONNECTED); + }, 1000); + }); + // Try opening a monitor connection with a timeout + return await Promise.race([connect, connectTimeout]); + } + + /** + * Pauses the currently running monitor, it still closes the gRPC connection + * with the underlying monitor process but it doesn't stop the message handlers + * currently running. + * This is mainly used to handle upload when to the board/port combination + * the monitor is listening to. + * @returns + */ + async pause(): Promise { + return new Promise(async (resolve) => { + if (!this.duplex) { + this.logger.warn( + `monitor to ${this.port?.address} using ${this.port?.protocol} already stopped` + ); + return resolve(); + } + // It's enough to close the connection with the client + // to stop the monitor process + this.duplex.end(); + this.duplex = null; + this.logger.info( + `stopped monitor to ${this.port?.address} using ${this.port?.protocol}` + ); + resolve(); + }); + } + + /** + * Stop the monitor currently running + */ + async stop(): Promise { + return this.pause().finally(this.stopMessagesHandlers.bind(this)); + } + + /** + * Send a message to the running monitor, a well behaved monitor + * will then send that message to the board. + * We MUST NEVER send a message that wasn't a user's input to the board. + * @param message string sent to running monitor + * @returns a status to verify message has been sent. + */ + async send(message: string): Promise { + if (!this.duplex) { + return Status.NOT_CONNECTED; + } + await this.coreClientProvider.initialized; + const coreClient = await this.coreClient(); + const { instance } = coreClient; + + const req = new MonitorRequest(); + req.setInstance(instance); + req.setTxData(new TextEncoder().encode(message)); + return new Promise((resolve) => { + if (this.duplex) { + this.duplex?.write(req, () => { + resolve(Status.OK); }); + return; + } + this.stop().then(() => resolve(Status.NOT_CONNECTED)); + }); + } - let settings: MonitorSettings = {}; - for (const iterator of res.getSettingsList()) { - settings[iterator.getSettingId()] = { - 'id': iterator.getSettingId(), - 'label': iterator.getLabel(), - 'type': iterator.getType(), - 'values': iterator.getEnumValuesList(), - 'selectedValue': iterator.getValue(), - } - } - return settings; + /** + * + * @returns map of current monitor settings + */ + currentSettings(): MonitorSettings { + return this.settings; + } + + /** + * Returns the possible configurations used to connect a monitor + * to the board specified by fqbn using the specified protocol + * @param protocol the protocol of the monitor we want get settings for + * @param fqbn the fqbn of the board we want to monitor + * @returns a map of all the settings supported by the monitor + */ + private async portMonitorSettings( + protocol: string, + fqbn: string + ): Promise { + await this.coreClientProvider.initialized; + const coreClient = await this.coreClient(); + const { client, instance } = coreClient; + const req = new EnumerateMonitorPortSettingsRequest(); + req.setInstance(instance); + req.setPortProtocol(protocol); + req.setFqbn(fqbn); + + const res = await new Promise( + (resolve, reject) => { + client.enumerateMonitorPortSettings(req, (err, resp) => { + if (!!err) { + reject(err); + } + resolve(resp); + }); + } + ); + + const settings: MonitorSettings = {}; + for (const iterator of res.getSettingsList()) { + settings[iterator.getSettingId()] = { + id: iterator.getSettingId(), + label: iterator.getLabel(), + type: iterator.getType(), + values: iterator.getEnumValuesList(), + selectedValue: iterator.getValue(), + }; + } + return settings; + } + + /** + * Set monitor settings, if there is a running monitor they'll be sent + * to it, otherwise they'll be used when starting one. + * Only values in settings parameter will be change, other values won't + * be changed in any way. + * @param settings map of monitor settings to change + * @returns a status to verify settings have been sent. + */ + async changeSettings(settings: MonitorSettings): Promise { + const config = new MonitorPortConfiguration(); + for (const id in settings) { + const s = new MonitorPortSetting(); + s.setSettingId(id); + s.setValue(settings[id].selectedValue); + config.addSettings(s); + this.settings[id] = settings[id]; } - /** - * Set monitor settings, if there is a running monitor they'll be sent - * to it, otherwise they'll be used when starting one. - * Only values in settings parameter will be change, other values won't - * be changed in any way. - * @param settings map of monitor settings to change - * @returns a status to verify settings have been sent. - */ - async changeSettings(settings: MonitorSettings): Promise { - const config = new MonitorPortConfiguration(); - for (const id in settings) { - const s = new MonitorPortSetting(); - s.setSettingId(id); - s.setValue(settings[id].selectedValue); - config.addSettings(s); - this.settings[id] = settings[id]; - } + if (!this.duplex) { + return Status.NOT_CONNECTED; + } + await this.coreClientProvider.initialized; + const coreClient = await this.coreClient(); + const { instance } = coreClient; - if (!this.duplex) { - return Status.NOT_CONNECTED; - } - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); - const { instance } = coreClient; + const req = new MonitorRequest(); + req.setInstance(instance); + req.setPortConfiguration(config); + this.duplex.write(req); + return Status.OK; + } - const req = new MonitorRequest(); - req.setInstance(instance); - req.setPortConfiguration(config) - this.duplex.write(req); - return Status.OK + /** + * Starts the necessary handlers to send and receive + * messages to and from the frontend and the running monitor + */ + private startMessagesHandlers(): void { + if (!this.flushMessagesInterval) { + const flushMessagesToFrontend = () => { + if (this.messages.length) { + this.webSocketProvider.sendMessage(JSON.stringify(this.messages)); + this.messages = []; + } + }; + this.flushMessagesInterval = setInterval(flushMessagesToFrontend, 32); } - /** - * Starts the necessary handlers to send and receive - * messages to and from the frontend and the running monitor - */ - private startMessagesHandlers(): void { - if (!this.flushMessagesInterval) { - const flushMessagesToFrontend = () => { - if (this.messages.length) { - this.webSocketProvider.sendMessage(JSON.stringify(this.messages)); - this.messages = []; - } - }; - this.flushMessagesInterval = setInterval(flushMessagesToFrontend, 32); - } + if (!this.onMessageReceived) { + this.onMessageReceived = this.webSocketProvider.onMessageReceived( + (msg: string) => { + const message: Monitor.Message = JSON.parse(msg); - if (!this.onMessageReceived) { - this.onMessageReceived = this.webSocketProvider.onMessageReceived( - (msg: string) => { - const message: Monitor.Message = JSON.parse(msg); - - switch (message.command) { - case Monitor.Command.SEND_MESSAGE: - this.send(message.data); - break - case Monitor.Command.CHANGE_SETTINGS: - const settings: MonitorSettings = JSON.parse(message.data); - this.changeSettings(settings); - break - } - } - ) + switch (message.command) { + case Monitor.Command.SEND_MESSAGE: + this.send(message.data); + break; + case Monitor.Command.CHANGE_SETTINGS: + const settings: MonitorSettings = JSON.parse(message.data); + this.changeSettings(settings); + break; + } } + ); } + } - /** - * Stops the necessary handlers to send and receive messages to - * and from the frontend and the running monitor - */ - private stopMessagesHandlers(): void { - if (this.flushMessagesInterval) { - clearInterval(this.flushMessagesInterval); - this.flushMessagesInterval = undefined; - } - if (this.onMessageReceived) { - this.onMessageReceived.dispose(); - this.onMessageReceived = undefined; - } + /** + * Stops the necessary handlers to send and receive messages to + * and from the frontend and the running monitor + */ + private stopMessagesHandlers(): void { + if (this.flushMessagesInterval) { + clearInterval(this.flushMessagesInterval); + this.flushMessagesInterval = undefined; } - + if (this.onMessageReceived) { + this.onMessageReceived.dispose(); + this.onMessageReceived = undefined; + } + } } /** @@ -368,5 +401,5 @@ export class MonitorService extends CoreClientAware implements Disposable { * @returns an lines array */ function splitLines(s: string): string[] { - return s.split(/(?<=\n)/); + return s.split(/(?<=\n)/); }