diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 143c7d6c5..5361bcb30 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -155,7 +155,11 @@ ], "arduino": { "cli": { - "version": "0.23.0" + "version": { + "owner": "arduino", + "repo": "arduino-cli", + "commitish": "c1b10f562f1e1a112e215a69b84e2f2b69e3af2d" + } }, "fwuploader": { "version": "2.2.0" diff --git a/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts b/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts index 78f444fe8..d693ce758 100644 --- a/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts +++ b/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts @@ -90,7 +90,7 @@ export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader { } catch (e) { throw e; } finally { - this.monitorManager.notifyUploadFinished(board, port); + await this.monitorManager.notifyUploadFinished(board, port); return output; } } diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index f6e39e4e1..6d55b307b 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -119,7 +119,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { return request; } - async upload(options: CoreService.Upload.Options): Promise { + upload(options: CoreService.Upload.Options): Promise { return this.doUpload( options, () => new UploadRequest(), diff --git a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts index c4e9d59d5..7f284ac3f 100644 --- a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts +++ b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts @@ -40,11 +40,14 @@ export class MonitorManagerProxyImpl implements MonitorManagerProxy { if (settings) { await this.changeMonitorSettings(board, port, settings); } - const status = await this.manager.startMonitor(board, port); - if (status === Status.ALREADY_CONNECTED || status === Status.OK) { - // Monitor started correctly, connect it with the frontend - this.client.connect(this.manager.getWebsocketAddressPort(board, port)); - } + + const connectToClient = (status: Status) => { + if (status === Status.ALREADY_CONNECTED || status === Status.OK) { + // Monitor started correctly, connect it with the frontend + this.client.connect(this.manager.getWebsocketAddressPort(board, port)); + } + }; + return this.manager.startMonitor(board, port, connectToClient); } /** diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index 5b5669059..fc458a49c 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -1,6 +1,6 @@ import { ILogger } from '@theia/core'; import { inject, injectable, named } from '@theia/core/shared/inversify'; -import { Board, Port, Status } from '../common/protocol'; +import { Board, BoardsService, Port, Status } from '../common/protocol'; import { CoreClientAware } from './core-client-provider'; import { MonitorService } from './monitor-service'; import { MonitorServiceFactory } from './monitor-service-factory'; @@ -11,16 +11,34 @@ import { type MonitorID = string; +type UploadState = 'uploadInProgress' | 'pausedForUpload' | 'disposedForUpload'; +type MonitorIDsByUploadState = Record; + export const MonitorManagerName = 'monitor-manager'; @injectable() export class MonitorManager extends CoreClientAware { + @inject(BoardsService) + protected boardsService: BoardsService; + // 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(); + private monitorIDsByUploadState: MonitorIDsByUploadState = { + uploadInProgress: [], + pausedForUpload: [], + disposedForUpload: [], + }; + + private monitorServiceStartQueue: { + monitorID: string; + serviceStartParams: [Board, Port]; + connectToClient: (status: Status) => void; + }[] = []; + @inject(MonitorServiceFactory) private monitorServiceFactory: MonitorServiceFactory; @@ -48,6 +66,33 @@ export class MonitorManager extends CoreClientAware { return false; } + private uploadIsInProgress(): boolean { + return this.monitorIDsByUploadState.uploadInProgress.length > 0; + } + + private addToMonitorIDsByUploadState( + state: UploadState, + monitorID: string + ): void { + this.monitorIDsByUploadState[state].push(monitorID); + } + + private removeFromMonitorIDsByUploadState( + state: UploadState, + monitorID: string + ): void { + this.monitorIDsByUploadState[state] = this.monitorIDsByUploadState[ + state + ].filter((id) => id !== monitorID); + } + + private monitorIDIsInUploadState( + state: UploadState, + monitorID: string + ): boolean { + return this.monitorIDsByUploadState[state].includes(monitorID); + } + /** * Start a pluggable monitor that receives and sends messages * to the specified board and port combination. @@ -56,13 +101,34 @@ export class MonitorManager extends CoreClientAware { * @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 { + async startMonitor( + board: Board, + port: Port, + connectToClient: (status: Status) => void + ): Promise { const monitorID = this.monitorID(board, port); + let monitor = this.monitorServices.get(monitorID); if (!monitor) { monitor = this.createMonitor(board, port); } - return await monitor.start(); + + if (this.uploadIsInProgress()) { + this.monitorServiceStartQueue = this.monitorServiceStartQueue.filter( + (request) => request.monitorID !== monitorID + ); + + this.monitorServiceStartQueue.push({ + monitorID, + serviceStartParams: [board, port], + connectToClient, + }); + + return; + } + + const result = await monitor.start(); + connectToClient(result); } /** @@ -111,14 +177,18 @@ export class MonitorManager extends CoreClientAware { // to retrieve if we don't have this information. return; } + const monitorID = this.monitorID(board, port); + this.addToMonitorIDsByUploadState('uploadInProgress', monitorID); + const monitor = this.monitorServices.get(monitorID); if (!monitor) { // There's no monitor running there, bail return; } - monitor.setUploadInProgress(true); - return await monitor.pause(); + + this.addToMonitorIDsByUploadState('pausedForUpload', monitorID); + return monitor.pause(); } /** @@ -130,19 +200,69 @@ export class MonitorManager extends CoreClientAware { * 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; + let status: Status = Status.NOT_CONNECTED; + let portDidChangeOnUpload = false; + + // We have no way of knowing which monitor + // to retrieve if we don't have this information. + if (board && port) { + const monitorID = this.monitorID(board, port); + this.removeFromMonitorIDsByUploadState('uploadInProgress', monitorID); + + const monitor = this.monitorServices.get(monitorID); + if (monitor) { + status = await monitor.start(); + } + + // this monitorID will only be present in "disposedForUpload" + // if the upload changed the board port + portDidChangeOnUpload = this.monitorIDIsInUploadState( + 'disposedForUpload', + monitorID + ); + if (portDidChangeOnUpload) { + this.removeFromMonitorIDsByUploadState('disposedForUpload', monitorID); + } + + // in case a service was paused but not disposed + this.removeFromMonitorIDsByUploadState('pausedForUpload', monitorID); } - 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; + + await this.startQueuedServices(portDidChangeOnUpload); + return status; + } + + async startQueuedServices(portDidChangeOnUpload: boolean): Promise { + // if the port changed during upload with the monitor open, "startMonitorPendingRequests" + // will include a request for our "upload port', most likely at index 0. + // We remove it, as this port was to be used exclusively for the upload + const queued = portDidChangeOnUpload + ? this.monitorServiceStartQueue.slice(1) + : this.monitorServiceStartQueue; + this.monitorServiceStartQueue = []; + + for (const { + monitorID, + serviceStartParams: [_, port], + connectToClient, + } of queued) { + const boardsState = await this.boardsService.getState(); + const boardIsStillOnPort = Object.keys(boardsState) + .map((connection: string) => { + const portAddress = connection.split('|')[0]; + return portAddress; + }) + .some((portAddress: string) => port.address === portAddress); + + if (boardIsStillOnPort) { + const monitorService = this.monitorServices.get(monitorID); + + if (monitorService) { + const result = await monitorService.start(); + connectToClient(result); + } + } } - monitor.setUploadInProgress(false); - return await monitor.start(); } /** @@ -202,6 +322,18 @@ export class MonitorManager extends CoreClientAware { this.monitorServices.set(monitorID, monitor); monitor.onDispose( (() => { + // if a service is disposed during upload and + // we paused it beforehand we know it was disposed + // of because the upload changed the board port + if ( + this.uploadIsInProgress() && + this.monitorIDIsInUploadState('pausedForUpload', monitorID) + ) { + this.removeFromMonitorIDsByUploadState('pausedForUpload', monitorID); + + this.addToMonitorIDsByUploadState('disposedForUpload', monitorID); + } + this.monitorServices.delete(monitorID); }).bind(this) ); diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index 086e98de7..a0ab86f6f 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -60,7 +60,6 @@ export class MonitorService extends CoreClientAware implements Disposable { protected readonly onDisposeEmitter = new Emitter(); readonly onDispose = this.onDisposeEmitter.event; - protected uploadInProgress = false; protected _initialized = new Deferred(); protected creating: Deferred; @@ -114,10 +113,6 @@ export class MonitorService extends CoreClientAware implements Disposable { return this._initialized.promise; } - setUploadInProgress(status: boolean): void { - this.uploadInProgress = status; - } - getWebsocketAddressPort(): number { return this.webSocketProvider.getAddress().port; } @@ -161,15 +156,6 @@ export class MonitorService extends CoreClientAware implements Disposable { return this.creating.promise; } - if (this.uploadInProgress) { - this.updateClientsSettings({ - monitorUISettings: { connected: false, serialPort: this.port.address }, - }); - - this.creating.resolve(Status.UPLOAD_IN_PROGRESS); - return this.creating.promise; - } - this.logger.info('starting monitor'); // get default monitor settings from the CLI