From e156dcc213221489241669f076937a1642a2ab16 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 27 Jul 2022 18:04:40 +0200 Subject: [PATCH] Show 'progress' indicator during verify/upload. Closes #575 Closes #1175 Signed-off-by: Akos Kitta --- .../browser/contributions/add-zip-library.ts | 9 +- .../browser/contributions/burn-bootloader.ts | 73 +++---- .../src/browser/contributions/contribution.ts | 41 +++- .../browser/contributions/upload-sketch.ts | 196 +++++++++--------- .../browser/contributions/verify-sketch.ts | 135 +++++++----- .../filterable-list-container.tsx | 5 +- .../src/common/protocol/core-service.ts | 58 +++--- .../src/common/protocol/installable.ts | 61 +----- .../src/common/protocol/progressible.ts | 60 ++++++ .../src/common/protocol/response-service.ts | 2 +- .../src/node/core-service-impl.ts | 190 +++++++++-------- .../src/node/grpc-progressible.ts | 128 +++++++++--- i18n/en.json | 3 + 13 files changed, 554 insertions(+), 407 deletions(-) create mode 100644 arduino-ide-extension/src/common/protocol/progressible.ts diff --git a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts index 8f5f32eb4..d3d3dbd3a 100644 --- a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts +++ b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts @@ -4,11 +4,8 @@ import URI from '@theia/core/lib/common/uri'; import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { ArduinoMenus } from '../menu/arduino-menus'; -import { - Installable, - LibraryService, - ResponseServiceClient, -} from '../../common/protocol'; +import { LibraryService, ResponseServiceClient } from '../../common/protocol'; +import { ExecuteWithProgress } from '../../common/protocol/progressible'; import { SketchContribution, Command, @@ -88,7 +85,7 @@ export class AddZipLibrary extends SketchContribution { private async doInstall(zipUri: string, overwrite?: boolean): Promise { try { - await Installable.doWithProgress({ + await ExecuteWithProgress.doWithProgress({ messageService: this.messageService, progressText: nls.localize('arduino/common/processing', 'Processing') + diff --git a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts index ef2ec75b1..2da1e20df 100644 --- a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts +++ b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts @@ -1,23 +1,16 @@ -import { inject, injectable } from '@theia/core/shared/inversify'; +import { nls } from '@theia/core/lib/common'; +import { injectable } from '@theia/core/shared/inversify'; +import { CoreService } from '../../common/protocol'; import { ArduinoMenus } from '../menu/arduino-menus'; -import { BoardsDataStore } from '../boards/boards-data-store'; -import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { - CoreServiceContribution, Command, CommandRegistry, + CoreServiceContribution, MenuModelRegistry, } from './contribution'; -import { nls } from '@theia/core/lib/common'; @injectable() export class BurnBootloader extends CoreServiceContribution { - @inject(BoardsDataStore) - protected readonly boardsDataStore: BoardsDataStore; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceClientImpl: BoardsServiceProvider; - override registerCommands(registry: CommandRegistry): void { registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, { execute: () => this.burnBootloader(), @@ -35,32 +28,19 @@ export class BurnBootloader extends CoreServiceContribution { }); } - async burnBootloader(): Promise { + private async burnBootloader(): Promise { + const options = await this.options(); try { - const { boardsConfig } = this.boardsServiceClientImpl; - const port = boardsConfig.selectedPort; - const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = - await Promise.all([ - this.boardsDataStore.appendConfigToFqbn( - boardsConfig.selectedBoard?.fqbn - ), - this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn), - this.preferences.get('arduino.upload.verify'), - this.preferences.get('arduino.upload.verbose'), - ]); - - const board = { - ...boardsConfig.selectedBoard, - name: boardsConfig.selectedBoard?.name || '', - fqbn, - }; - this.outputChannelManager.getChannel('Arduino').clear(); - await this.coreService.burnBootloader({ - board, - programmer, - port, - verify, - verbose, + await this.doWithProgress({ + progressText: nls.localize( + 'arduino/bootloader/burningBootloader', + 'Burning bootloader...' + ), + task: (progressId, coreService) => + coreService.burnBootloader({ + ...options, + progressId, + }), }); this.messageService.info( nls.localize( @@ -75,6 +55,27 @@ export class BurnBootloader extends CoreServiceContribution { this.handleError(e); } } + + private async options(): Promise { + const { boardsConfig } = this.boardsServiceProvider; + const port = boardsConfig.selectedPort; + const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = + await Promise.all([ + this.boardsDataStore.appendConfigToFqbn( + boardsConfig.selectedBoard?.fqbn + ), + this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn), + this.preferences.get('arduino.upload.verify'), + this.preferences.get('arduino.upload.verbose'), + ]); + return { + fqbn, + programmer, + port, + verify, + verbose, + }; + } } export namespace BurnBootloader { diff --git a/arduino-ide-extension/src/browser/contributions/contribution.ts b/arduino-ide-extension/src/browser/contributions/contribution.ts index 096071047..d3ae285f7 100644 --- a/arduino-ide-extension/src/browser/contributions/contribution.ts +++ b/arduino-ide-extension/src/browser/contributions/contribution.ts @@ -49,13 +49,16 @@ import { Sketch, CoreService, CoreError, + ResponseServiceClient, } from '../../common/protocol'; import { ArduinoPreferences } from '../arduino-preferences'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; -import { CoreErrorHandler } from './core-error-handler'; import { nls } from '@theia/core'; import { OutputChannelManager } from '../theia/output/output-channel'; import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; +import { ExecuteWithProgress } from '../../common/protocol/progressible'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; +import { BoardsDataStore } from '../boards/boards-data-store'; export { Command, @@ -167,18 +170,23 @@ export abstract class SketchContribution extends Contribution { } @injectable() -export class CoreServiceContribution extends SketchContribution { - @inject(CoreService) - protected readonly coreService: CoreService; +export abstract class CoreServiceContribution extends SketchContribution { + @inject(BoardsDataStore) + protected readonly boardsDataStore: BoardsDataStore; + + @inject(BoardsServiceProvider) + protected readonly boardsServiceProvider: BoardsServiceProvider; - @inject(CoreErrorHandler) - protected readonly coreErrorHandler: CoreErrorHandler; + @inject(CoreService) + private readonly coreService: CoreService; @inject(ClipboardService) private readonly clipboardService: ClipboardService; + @inject(ResponseServiceClient) + private readonly responseService: ResponseServiceClient; + protected handleError(error: unknown): void { - this.coreErrorHandler.tryHandle(error); this.tryToastErrorMessage(error); } @@ -214,6 +222,25 @@ export class CoreServiceContribution extends SketchContribution { throw error; } } + + protected async doWithProgress(options: { + progressText: string; + keepOutput?: boolean; + task: (progressId: string, coreService: CoreService) => Promise; + }): Promise { + const { progressText, keepOutput, task } = options; + this.outputChannelManager + .getChannel('Arduino') + .show({ preserveFocus: true }); + const result = await ExecuteWithProgress.doWithProgress({ + messageService: this.messageService, + responseService: this.responseService, + progressText, + run: ({ progressId }) => task(progressId, this.coreService), + keepOutput, + }); + return result; + } } export namespace Contribution { diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index c17143bbf..e15e612ad 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -3,56 +3,47 @@ import { Emitter } from '@theia/core/lib/common/event'; import { BoardUserField, CoreService } from '../../common/protocol'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; -import { BoardsDataStore } from '../boards/boards-data-store'; -import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { - CoreServiceContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, + CoreServiceContribution, } from './contribution'; import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog'; import { DisposableCollection, nls } from '@theia/core/lib/common'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; +import type { VerifySketchParams } from './verify-sketch'; @injectable() export class UploadSketch extends CoreServiceContribution { @inject(MenuModelRegistry) - protected readonly menuRegistry: MenuModelRegistry; - - @inject(BoardsDataStore) - protected readonly boardsDataStore: BoardsDataStore; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceClientImpl: BoardsServiceProvider; + private readonly menuRegistry: MenuModelRegistry; @inject(UserFieldsDialog) - protected readonly userFieldsDialog: UserFieldsDialog; - - protected cachedUserFields: Map = new Map(); - - protected readonly onDidChangeEmitter = new Emitter>(); - readonly onDidChange = this.onDidChangeEmitter.event; + private readonly userFieldsDialog: UserFieldsDialog; - protected uploadInProgress = false; - protected boardRequiresUserFields = false; + private boardRequiresUserFields = false; + private readonly cachedUserFields: Map = new Map(); + private readonly menuActionsDisposables = new DisposableCollection(); - protected readonly menuActionsDisposables = new DisposableCollection(); + private readonly onDidChangeEmitter = new Emitter(); + private readonly onDidChange = this.onDidChangeEmitter.event; + private uploadInProgress = false; protected override init(): void { super.init(); - this.boardsServiceClientImpl.onBoardsConfigChanged(async () => { + this.boardsServiceProvider.onBoardsConfigChanged(async () => { const userFields = - await this.boardsServiceClientImpl.selectedBoardUserFields(); + await this.boardsServiceProvider.selectedBoardUserFields(); this.boardRequiresUserFields = userFields.length > 0; this.registerMenus(this.menuRegistry); }); } private selectedFqbnAddress(): string { - const { boardsConfig } = this.boardsServiceClientImpl; + const { boardsConfig } = this.boardsServiceProvider; const fqbn = boardsConfig.selectedBoard?.fqbn; if (!fqbn) { return ''; @@ -76,7 +67,7 @@ export class UploadSketch extends CoreServiceContribution { if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) { // Deep clone the array of board fields to avoid editing the cached ones this.userFieldsDialog.value = ( - await this.boardsServiceClientImpl.selectedBoardUserFields() + await this.boardsServiceProvider.selectedBoardUserFields() ).map((f) => ({ ...f })); const result = await this.userFieldsDialog.open(); if (!result) { @@ -98,8 +89,7 @@ export class UploadSketch extends CoreServiceContribution { const cached = this.cachedUserFields.get(key); // Deep clone the array of board fields to avoid editing the cached ones this.userFieldsDialog.value = ( - cached ?? - (await this.boardsServiceClientImpl.selectedBoardUserFields()) + cached ?? (await this.boardsServiceProvider.selectedBoardUserFields()) ).map((f) => ({ ...f })); const result = await this.userFieldsDialog.open(); @@ -130,7 +120,6 @@ export class UploadSketch extends CoreServiceContribution { override registerMenus(registry: MenuModelRegistry): void { this.menuActionsDisposables.dispose(); - this.menuActionsDisposables.push( registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { commandId: UploadSketch.Commands.UPLOAD_SKETCH.id, @@ -153,7 +142,7 @@ export class UploadSketch extends CoreServiceContribution { new PlaceholderMenuNode( ArduinoMenus.SKETCH__MAIN_GROUP, // commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id, - UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label!, + UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label, { order: '2' } ) ) @@ -193,57 +182,42 @@ export class UploadSketch extends CoreServiceContribution { } async uploadSketch(usingProgrammer = false): Promise { - // even with buttons disabled, better to double check if an upload is already in progress if (this.uploadInProgress) { return; } - const sketch = await this.sketchServiceClient.currentSketch(); - if (!CurrentSketch.isValid(sketch)) { - return; - } - try { // toggle the toolbar button and menu item state. // uploadInProgress will be set to false whether the upload fails or not this.uploadInProgress = true; - this.coreErrorHandler.reset(); this.onDidChangeEmitter.fire(); - const { boardsConfig } = this.boardsServiceClientImpl; - const [ - fqbn, - { selectedProgrammer }, - verify, - uploadVerbose, - sourceOverride, - optimizeForDebug, - compileVerbose, - ] = await Promise.all([ - this.boardsDataStore.appendConfigToFqbn( - boardsConfig.selectedBoard?.fqbn - ), - this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn), - this.preferences.get('arduino.upload.verify'), - this.preferences.get('arduino.upload.verbose'), - this.sourceOverride(), - this.commandService.executeCommand( - 'arduino-is-optimize-for-debug' - ), - this.preferences.get('arduino.compile.verbose'), - ]); - const verbose = { compile: compileVerbose, upload: uploadVerbose }; - const board = { - ...boardsConfig.selectedBoard, - name: boardsConfig.selectedBoard?.name || '', - fqbn, - }; - let options: CoreService.Upload.Options | undefined = undefined; - const { selectedPort } = boardsConfig; - const port = selectedPort; - const userFields = - this.cachedUserFields.get(this.selectedFqbnAddress()) ?? []; - if (userFields.length === 0 && this.boardRequiresUserFields) { + const verifyOptions = + await this.commandService.executeCommand( + 'arduino-verify-sketch', + { + exportBinaries: false, + silent: true, + } + ); + if (!verifyOptions) { + return; + } + + const uploadOptions = await this.uploadOptions( + usingProgrammer, + verifyOptions + ); + if (!uploadOptions) { + return; + } + + // TODO: This does not belong here. + // IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message. + if ( + uploadOptions.userFields.length === 0 && + this.boardRequiresUserFields + ) { this.messageService.error( nls.localize( 'arduino/sketch/userFieldsNotFoundError', @@ -253,37 +227,13 @@ export class UploadSketch extends CoreServiceContribution { return; } - if (usingProgrammer) { - const programmer = selectedProgrammer; - options = { - sketch, - board, - optimizeForDebug: Boolean(optimizeForDebug), - programmer, - port, - verbose, - verify, - sourceOverride, - userFields, - }; - } else { - options = { - sketch, - board, - optimizeForDebug: Boolean(optimizeForDebug), - port, - verbose, - verify, - sourceOverride, - userFields, - }; - } - this.outputChannelManager.getChannel('Arduino').clear(); - if (usingProgrammer) { - await this.coreService.uploadUsingProgrammer(options); - } else { - await this.coreService.upload(options); - } + await this.doWithProgress({ + progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'), + task: (progressId, coreService) => + coreService.upload({ ...uploadOptions, progressId }), + keepOutput: true, + }); + this.messageService.info( nls.localize('arduino/sketch/doneUploading', 'Done uploading.'), { timeout: 3000 } @@ -295,6 +245,52 @@ export class UploadSketch extends CoreServiceContribution { this.onDidChangeEmitter.fire(); } } + + private async uploadOptions( + usingProgrammer: boolean, + verifyOptions: CoreService.Options.Compile + ): Promise { + const sketch = await this.sketchServiceClient.currentSketch(); + if (!CurrentSketch.isValid(sketch)) { + return undefined; + } + const userFields = this.userFields(); + const { boardsConfig } = this.boardsServiceProvider; + const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = + await Promise.all([ + verifyOptions.fqbn, // already decorated FQBN + this.boardsDataStore.getData(this.sanitizeFqbn(verifyOptions.fqbn)), + this.preferences.get('arduino.upload.verify'), + this.preferences.get('arduino.upload.verbose'), + ]); + const port = boardsConfig.selectedPort; + return { + sketch, + fqbn, + ...(usingProgrammer && { programmer }), + port, + verbose, + verify, + userFields, + }; + } + + private userFields() { + return this.cachedUserFields.get(this.selectedFqbnAddress()) ?? []; + } + + /** + * Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to + * `VENDOR:ARCHITECTURE:BOARD_ID` format. + * See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties). + */ + private sanitizeFqbn(fqbn: string | undefined): string | undefined { + if (!fqbn) { + return undefined; + } + const [vendor, arch, id] = fqbn.split(':'); + return `${vendor}:${arch}:${id}`; + } } export namespace UploadSketch { @@ -302,7 +298,7 @@ export namespace UploadSketch { export const UPLOAD_SKETCH: Command = { id: 'arduino-upload-sketch', }; - export const UPLOAD_WITH_CONFIGURATION: Command = { + export const UPLOAD_WITH_CONFIGURATION: Command & { label: string } = { id: 'arduino-upload-with-configuration-sketch', label: nls.localize( 'arduino/sketch/configureAndUpload', diff --git a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts index a7cd1e197..3fc2d53a0 100644 --- a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts @@ -2,8 +2,6 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { Emitter } from '@theia/core/lib/common/event'; import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; -import { BoardsDataStore } from '../boards/boards-data-store'; -import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { CoreServiceContribution, Command, @@ -14,27 +12,36 @@ import { } from './contribution'; import { nls } from '@theia/core/lib/common'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; +import { CoreService } from '../../common/protocol'; +import { CoreErrorHandler } from './core-error-handler'; + +export interface VerifySketchParams { + /** + * Same as `CoreService.Options.Compile#exportBinaries` + */ + readonly exportBinaries?: boolean; + /** + * If `true`, there won't be any UI indication of the verify command. It's `false` by default. + */ + readonly silent?: boolean; +} @injectable() export class VerifySketch extends CoreServiceContribution { - @inject(BoardsDataStore) - protected readonly boardsDataStore: BoardsDataStore; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceClientImpl: BoardsServiceProvider; + @inject(CoreErrorHandler) + private readonly coreErrorHandler: CoreErrorHandler; - protected readonly onDidChangeEmitter = new Emitter>(); - readonly onDidChange = this.onDidChangeEmitter.event; - - protected verifyInProgress = false; + private readonly onDidChangeEmitter = new Emitter(); + private readonly onDidChange = this.onDidChangeEmitter.event; + private verifyInProgress = false; override registerCommands(registry: CommandRegistry): void { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, { - execute: () => this.verifySketch(), + execute: (params?: VerifySketchParams) => this.verifySketch(params), isEnabled: () => !this.verifyInProgress, }); registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, { - execute: () => this.verifySketch(true), + execute: () => this.verifySketch({ exportBinaries: true }), isEnabled: () => !this.verifyInProgress, }); registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, { @@ -84,61 +91,87 @@ export class VerifySketch extends CoreServiceContribution { }); } - async verifySketch(exportBinaries?: boolean): Promise { - // even with buttons disabled, better to double check if a verify is already in progress + protected override handleError(error: unknown): void { + this.coreErrorHandler.tryHandle(error); + super.handleError(error); + } + + private async verifySketch( + params?: VerifySketchParams + ): Promise { if (this.verifyInProgress) { - return; + return undefined; } - // toggle the toolbar button and menu item state. - // verifyInProgress will be set to false whether the compilation fails or not - const sketch = await this.sketchServiceClient.currentSketch(); - if (!CurrentSketch.isValid(sketch)) { - return; - } try { - this.verifyInProgress = true; + if (!params?.silent) { + this.verifyInProgress = true; + this.onDidChangeEmitter.fire(); + } this.coreErrorHandler.reset(); - this.onDidChangeEmitter.fire(); - const { boardsConfig } = this.boardsServiceClientImpl; - const [fqbn, sourceOverride] = await Promise.all([ - this.boardsDataStore.appendConfigToFqbn( - boardsConfig.selectedBoard?.fqbn + + const options = await this.options(params?.exportBinaries); + if (!options) { + return undefined; + } + + await this.doWithProgress({ + progressText: nls.localize( + 'arduino/sketch/compile', + 'Compiling sketch...' ), - this.sourceOverride(), - ]); - const board = { - ...boardsConfig.selectedBoard, - name: boardsConfig.selectedBoard?.name || '', - fqbn, - }; - const verbose = this.preferences.get('arduino.compile.verbose'); - const compilerWarnings = this.preferences.get('arduino.compile.warnings'); - const optimizeForDebug = - await this.commandService.executeCommand( - 'arduino-is-optimize-for-debug' - ); - this.outputChannelManager.getChannel('Arduino').clear(); - await this.coreService.compile({ - sketch, - board, - optimizeForDebug: Boolean(optimizeForDebug), - verbose, - exportBinaries, - sourceOverride, - compilerWarnings, + task: (progressId, coreService) => + coreService.compile({ + ...options, + progressId, + }), }); this.messageService.info( nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'), { timeout: 3000 } ); + // Returns with the used options for the compilation + // so that follow-up tasks (such as upload) can reuse the compiled code. + // Note that the `fqbn` is already decorated with the board settings, if any. + return options; } catch (e) { this.handleError(e); + return undefined; } finally { this.verifyInProgress = false; - this.onDidChangeEmitter.fire(); + if (!params?.silent) { + this.onDidChangeEmitter.fire(); + } } } + + private async options( + exportBinaries?: boolean + ): Promise { + const sketch = await this.sketchServiceClient.currentSketch(); + if (!CurrentSketch.isValid(sketch)) { + return undefined; + } + const { boardsConfig } = this.boardsServiceProvider; + const [fqbn, sourceOverride, optimizeForDebug] = await Promise.all([ + this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn), + this.sourceOverride(), + this.commandService.executeCommand( + 'arduino-is-optimize-for-debug' + ), + ]); + const verbose = this.preferences.get('arduino.compile.verbose'); + const compilerWarnings = this.preferences.get('arduino.compile.warnings'); + return { + sketch, + fqbn, + optimizeForDebug: Boolean(optimizeForDebug), + verbose, + exportBinaries, + sourceOverride, + compilerWarnings, + }; + } } export namespace VerifySketch { diff --git a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx index 194e0405c..ce132bc5f 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx @@ -5,6 +5,7 @@ import { CommandService } from '@theia/core/lib/common/command'; import { MessageService } from '@theia/core/lib/common/message-service'; import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; import { Searchable } from '../../../common/protocol/searchable'; +import { ExecuteWithProgress } from '../../../common/protocol/progressible'; import { Installable } from '../../../common/protocol/installable'; import { ArduinoComponent } from '../../../common/protocol/arduino-component'; import { SearchBar } from './search-bar'; @@ -111,7 +112,7 @@ export class FilterableListContainer< version: Installable.Version ): Promise { const { install, searchable } = this.props; - await Installable.doWithProgress({ + await ExecuteWithProgress.doWithProgress({ ...this.props, progressText: nls.localize('arduino/common/processing', 'Processing') + @@ -137,7 +138,7 @@ export class FilterableListContainer< return; } const { uninstall, searchable } = this.props; - await Installable.doWithProgress({ + await ExecuteWithProgress.doWithProgress({ ...this.props, progressText: nls.localize('arduino/common/processing', 'Processing') + diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index 1ac6a2913..4c85ecc5c 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -1,7 +1,6 @@ import { ApplicationError } from '@theia/core/lib/common/application-error'; import type { Location } from '@theia/core/shared/vscode-languageserver-protocol'; import type { - Board, BoardUserField, Port, } from '../../common/protocol/boards-service'; @@ -60,46 +59,39 @@ export namespace CoreError { export const CoreServicePath = '/services/core-service'; export const CoreService = Symbol('CoreService'); export interface CoreService { - compile( - options: CoreService.Compile.Options & - Readonly<{ - exportBinaries?: boolean; - compilerWarnings?: CompilerWarnings; - }> - ): Promise; - upload(options: CoreService.Upload.Options): Promise; - uploadUsingProgrammer(options: CoreService.Upload.Options): Promise; - burnBootloader(options: CoreService.Bootloader.Options): Promise; + compile(options: CoreService.Options.Compile): Promise; + upload(options: CoreService.Options.Upload): Promise; + burnBootloader(options: CoreService.Options.Bootloader): Promise; } export namespace CoreService { - export namespace Compile { - export interface Options { + export namespace Options { + export interface Base { + readonly fqbn?: string | undefined; + readonly verbose: boolean; // TODO: (API) why not optional with a default false? + readonly progressId?: string; + } + export interface SketchBased { readonly sketch: Sketch; - readonly board?: Board; - readonly optimizeForDebug: boolean; - readonly verbose: boolean; - readonly sourceOverride: Record; } - } - - export namespace Upload { - export interface Options extends Omit { + export interface BoardBased { readonly port?: Port; readonly programmer?: Programmer | undefined; - readonly verify: boolean; - readonly userFields: BoardUserField[]; - readonly verbose: { compile: boolean; upload: boolean }; + /** + * For the _Verify after upload_ setting. + */ + readonly verify: boolean; // TODO: (API) why not optional with false as the default value? } - } - - export namespace Bootloader { - export interface Options { - readonly board?: Board; - readonly port?: Port; - readonly programmer?: Programmer | undefined; - readonly verbose: boolean; - readonly verify: boolean; + export interface Compile extends Base, SketchBased { + readonly optimizeForDebug: boolean; // TODO: (API) make this optional + readonly sourceOverride: Record; // TODO: (API) make this optional + readonly exportBinaries?: boolean; + readonly compilerWarnings?: CompilerWarnings; + } + export interface Upload extends Base, SketchBased, BoardBased { + readonly userFields: BoardUserField[]; + readonly usingProgrammer?: boolean; } + export interface Bootloader extends Base, BoardBased {} } } diff --git a/arduino-ide-extension/src/common/protocol/installable.ts b/arduino-ide-extension/src/common/protocol/installable.ts index 7d8a7e363..527805697 100644 --- a/arduino-ide-extension/src/common/protocol/installable.ts +++ b/arduino-ide-extension/src/common/protocol/installable.ts @@ -1,10 +1,6 @@ import * as semver from 'semver'; -import type { Progress } from '@theia/core/lib/common/message-service-protocol'; -import { - CancellationToken, - CancellationTokenSource, -} from '@theia/core/lib/common/cancellation'; -import { naturalCompare } from './../utils'; +import { ExecuteWithProgress } from './progressible'; +import { naturalCompare } from '../utils'; import type { ArduinoComponent } from './arduino-component'; import type { MessageService } from '@theia/core/lib/common/message-service'; import type { ResponseServiceClient } from './response-service'; @@ -32,7 +28,7 @@ export namespace Installable { /** * Most recent version comes first, then the previous versions. (`1.8.1`, `1.6.3`, `1.6.2`, `1.6.1` and so on.) */ - export const COMPARATOR = (left: Version, right: Version) => { + export const COMPARATOR = (left: Version, right: Version): number => { if (semver.valid(left) && semver.valid(right)) { return semver.compare(left, right); } @@ -50,7 +46,7 @@ export namespace Installable { version: Installable.Version; }): Promise { const { item, version } = options; - return doWithProgress({ + return ExecuteWithProgress.doWithProgress({ ...options, progressText: `Processing ${item.name}:${version}`, run: ({ progressId }) => @@ -71,7 +67,7 @@ export namespace Installable { item: T; }): Promise { const { item } = options; - return doWithProgress({ + return ExecuteWithProgress.doWithProgress({ ...options, progressText: `Processing ${item.name}${ item.installedVersion ? `:${item.installedVersion}` : '' @@ -83,51 +79,4 @@ export namespace Installable { }), }); } - - export async function doWithProgress(options: { - run: ({ progressId }: { progressId: string }) => Promise; - messageService: MessageService; - responseService: ResponseServiceClient; - progressText: string; - }): Promise { - return withProgress( - options.progressText, - options.messageService, - async (progress, _) => { - const progressId = progress.id; - const toDispose = options.responseService.onProgressDidChange( - (progressMessage) => { - if (progressId === progressMessage.progressId) { - const { message, work } = progressMessage; - progress.report({ message, work }); - } - } - ); - try { - options.responseService.clearOutput(); - await options.run({ progressId }); - } finally { - toDispose.dispose(); - } - } - ); - } - - async function withProgress( - text: string, - messageService: MessageService, - cb: (progress: Progress, token: CancellationToken) => Promise - ): Promise { - const cancellationSource = new CancellationTokenSource(); - const { token } = cancellationSource; - const progress = await messageService.showProgress( - { text, options: { cancelable: false } }, - () => cancellationSource.cancel() - ); - try { - await cb(progress, token); - } finally { - progress.cancel(); - } - } } diff --git a/arduino-ide-extension/src/common/protocol/progressible.ts b/arduino-ide-extension/src/common/protocol/progressible.ts new file mode 100644 index 000000000..03e5141ef --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/progressible.ts @@ -0,0 +1,60 @@ +import type { CancellationToken } from '@theia/core/lib/common/cancellation'; +import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; +import type { MessageService } from '@theia/core/lib/common/message-service'; +import type { Progress } from '@theia/core/lib/common/message-service-protocol'; +import type { ResponseServiceClient } from './response-service'; + +export namespace ExecuteWithProgress { + export async function doWithProgress(options: { + run: ({ progressId }: { progressId: string }) => Promise; + messageService: MessageService; + responseService: ResponseServiceClient; + progressText: string; + keepOutput?: boolean; + }): Promise { + return withProgress( + options.progressText, + options.messageService, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async (progress, _token) => { + const progressId = progress.id; + const toDispose = options.responseService.onProgressDidChange( + (progressMessage) => { + if (progressId === progressMessage.progressId) { + const { message, work } = progressMessage; + progress.report({ message, work }); + } + } + ); + try { + if (!options.keepOutput) { + options.responseService.clearOutput(); + } + const result = await options.run({ progressId }); + return result; + } finally { + toDispose.dispose(); + } + } + ); + } + + async function withProgress( + text: string, + messageService: MessageService, + cb: (progress: Progress, token: CancellationToken) => Promise + ): Promise { + const cancellationSource = new CancellationTokenSource(); + const { token } = cancellationSource; + const progress = await messageService.showProgress( + { text, options: { cancelable: false } }, + () => cancellationSource.cancel() + ); + try { + const result = await cb(progress, token); + return result; + } finally { + progress.cancel(); + } + } +} diff --git a/arduino-ide-extension/src/common/protocol/response-service.ts b/arduino-ide-extension/src/common/protocol/response-service.ts index d54be98f7..b39d9ba40 100644 --- a/arduino-ide-extension/src/common/protocol/response-service.ts +++ b/arduino-ide-extension/src/common/protocol/response-service.ts @@ -46,5 +46,5 @@ export interface ResponseService { export const ResponseServiceClient = Symbol('ResponseServiceClient'); export interface ResponseServiceClient extends ResponseService { onProgressDidChange: Event; - clearOutput: () => void; + clearOutput: () => void; // TODO: this should not belong here. } diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index a10b829da..7072a34dc 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -33,6 +33,12 @@ import { tryParseError } from './cli-error-parser'; import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; import { firstToUpperCase, notEmpty } from '../common/utils'; import { ServiceError } from './service-error'; +import { ExecuteWithProgress, ProgressResponse } from './grpc-progressible'; + +namespace Uploadable { + export type Request = UploadRequest | UploadUsingProgrammerRequest; + export type Response = UploadResponse | UploadUsingProgrammerResponse; +} @injectable() export class CoreServiceImpl extends CoreClientAware implements CoreService { @@ -45,27 +51,27 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { @inject(CommandService) private readonly commandService: CommandService; - async compile( - options: CoreService.Compile.Options & { - exportBinaries?: boolean; - compilerWarnings?: CompilerWarnings; - } - ): Promise { + async compile(options: CoreService.Options.Compile): Promise { const coreClient = await this.coreClient; const { client, instance } = coreClient; let buildPath: string | undefined = undefined; - const handler = this.createOnDataHandler((response) => { + const progressHandler = this.createProgressHandler(options); + const buildPathHandler = (response: CompileResponse) => { const currentBuildPath = response.getBuildPath(); - if (!buildPath && currentBuildPath) { + if (currentBuildPath) { buildPath = currentBuildPath; } else { - if (!!currentBuildPath && currentBuildPath !== buildPath) { + if (!!buildPath && currentBuildPath !== buildPath) { throw new Error( - `The CLI has already provided a build path: <${buildPath}>, and there is a new build path value: <${currentBuildPath}>.` + `The CLI has already provided a build path: <${buildPath}>, and IDE2 received a new build path value: <${currentBuildPath}>.` ); } } - }); + }; + const handler = this.createOnDataHandler( + progressHandler, + buildPathHandler + ); const request = this.compileRequest(options, instance); return new Promise((resolve, reject) => { client @@ -132,20 +138,20 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { } private compileRequest( - options: CoreService.Compile.Options & { + options: CoreService.Options.Compile & { exportBinaries?: boolean; compilerWarnings?: CompilerWarnings; }, instance: Instance ): CompileRequest { - const { sketch, board, compilerWarnings } = options; + const { sketch, fqbn, compilerWarnings } = options; const sketchUri = sketch.uri; const sketchPath = FileUri.fsPath(sketchUri); const request = new CompileRequest(); request.setInstance(instance); request.setSketchPath(sketchPath); - if (board?.fqbn) { - request.setFqbn(board.fqbn); + if (fqbn) { + request.setFqbn(fqbn); } if (compilerWarnings) { request.setWarnings(compilerWarnings.toLowerCase()); @@ -163,60 +169,44 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { return request; } - upload(options: CoreService.Upload.Options): Promise { - return this.doUpload( - options, - () => new UploadRequest(), - (client, req) => client.upload(req), - (message: string, locations: CoreError.ErrorLocation[]) => - CoreError.UploadFailed(message, locations), - 'upload' - ); - } - - async uploadUsingProgrammer( - options: CoreService.Upload.Options - ): Promise { + upload(options: CoreService.Options.Upload): Promise { + const { usingProgrammer } = options; return this.doUpload( options, - () => new UploadUsingProgrammerRequest(), - (client, req) => client.uploadUsingProgrammer(req), - (message: string, locations: CoreError.ErrorLocation[]) => - CoreError.UploadUsingProgrammerFailed(message, locations), - 'upload using programmer' + usingProgrammer + ? new UploadUsingProgrammerRequest() + : new UploadRequest(), + (client) => + (usingProgrammer ? client.uploadUsingProgrammer : client.upload).bind( + client + ), + usingProgrammer + ? CoreError.UploadUsingProgrammerFailed + : CoreError.UploadFailed, + `upload${usingProgrammer ? ' using programmer' : ''}` ); } - protected async doUpload( - options: CoreService.Upload.Options, - requestFactory: () => UploadRequest | UploadUsingProgrammerRequest, - responseHandler: ( - client: ArduinoCoreServiceClient, - request: UploadRequest | UploadUsingProgrammerRequest - ) => ClientReadableStream, - errorHandler: ( - message: string, - locations: CoreError.ErrorLocation[] - ) => ApplicationError, + protected async doUpload< + REQ extends Uploadable.Request, + RESP extends Uploadable.Response + >( + options: CoreService.Options.Upload, + request: REQ, + responseFactory: ( + client: ArduinoCoreServiceClient + ) => (request: REQ) => ClientReadableStream, + errorCtor: ApplicationError.Constructor, task: string ): Promise { - await this.compile({ - ...options, - verbose: options.verbose.compile, - exportBinaries: false, - }); - const coreClient = await this.coreClient; const { client, instance } = coreClient; - const request = this.uploadOrUploadUsingProgrammerRequest( - options, - instance, - requestFactory - ); - const handler = this.createOnDataHandler(); + const progressHandler = this.createProgressHandler(options); + const handler = this.createOnDataHandler(progressHandler); + const grpcCall = responseFactory(client); return this.notifyUploadWillStart(options).then(() => new Promise((resolve, reject) => { - responseHandler(client, request) + grpcCall(this.initUploadRequest(request, options, instance)) .on('data', handler.onData) .on('error', (error) => { if (!ServiceError.is(error)) { @@ -231,7 +221,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { ); this.sendResponse(error.details, OutputMessage.Severity.Error); reject( - errorHandler( + errorCtor( message, tryParseError({ content: handler.stderr, @@ -249,24 +239,23 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { ); } - private uploadOrUploadUsingProgrammerRequest( - options: CoreService.Upload.Options, - instance: Instance, - requestFactory: () => UploadRequest | UploadUsingProgrammerRequest - ): UploadRequest | UploadUsingProgrammerRequest { - const { sketch, board, port, programmer } = options; + private initUploadRequest( + request: REQ, + options: CoreService.Options.Upload, + instance: Instance + ): REQ { + const { sketch, fqbn, port, programmer } = options; const sketchPath = FileUri.fsPath(sketch.uri); - const request = requestFactory(); request.setInstance(instance); request.setSketchPath(sketchPath); - if (board?.fqbn) { - request.setFqbn(board.fqbn); + if (fqbn) { + request.setFqbn(fqbn); } request.setPort(this.createPort(port)); if (programmer) { request.setProgrammer(programmer.id); } - request.setVerbose(options.verbose.upload); + request.setVerbose(options.verbose); request.setVerify(options.verify); options.userFields.forEach((e) => { @@ -275,10 +264,11 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { return request; } - async burnBootloader(options: CoreService.Bootloader.Options): Promise { + async burnBootloader(options: CoreService.Options.Bootloader): Promise { const coreClient = await this.coreClient; const { client, instance } = coreClient; - const handler = this.createOnDataHandler(); + const progressHandler = this.createProgressHandler(options); + const handler = this.createOnDataHandler(progressHandler); const request = this.burnBootloaderRequest(options, instance); return this.notifyUploadWillStart(options).then(() => new Promise((resolve, reject) => { @@ -315,14 +305,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { } private burnBootloaderRequest( - options: CoreService.Bootloader.Options, + options: CoreService.Options.Bootloader, instance: Instance ): BurnBootloaderRequest { - const { board, port, programmer } = options; + const { fqbn, port, programmer } = options; const request = new BurnBootloaderRequest(); request.setInstance(instance); - if (board?.fqbn) { - request.setFqbn(board.fqbn); + if (fqbn) { + request.setFqbn(fqbn); } request.setPort(this.createPort(port)); if (programmer) { @@ -333,8 +323,24 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { return request; } + private createProgressHandler( + options: CoreService.Options.Base + ): (response: R) => void { + // If client did not provide the progress ID, do nothing. + if (!options.progressId) { + return () => { + /* NOOP */ + }; + } + return ExecuteWithProgress.createDataCallback({ + progressId: options.progressId, + responseService: this.responseService, + }); + } + private createOnDataHandler( - onResponse?: (response: R) => void + // TODO: why not creating a composite handler with progress, `build_path`, and out/err stream handlers? + ...handlers: ((response: R) => void)[] ): Disposable & { stderr: Buffer[]; onData: (response: R) => void; @@ -347,14 +353,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { } }); }); - const onData = StreamingResponse.createOnDataHandler( + const onData = StreamingResponse.createOnDataHandler({ stderr, - (out, err) => { + onData: (out, err) => { buffer.addChunk(out); buffer.addChunk(err, OutputMessage.Severity.Error); }, - onResponse - ); + handlers, + }); return { dispose: () => buffer.dispose(), stderr, @@ -391,7 +397,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { private mergeSourceOverrides( req: { getSourceOverrideMap(): jspb.Map }, - options: CoreService.Compile.Options + options: CoreService.Options.Compile ): void { const sketchPath = FileUri.fsPath(options.sketch.uri); for (const uri of Object.keys(options.sourceOverride)) { @@ -422,18 +428,24 @@ type StreamingResponse = namespace StreamingResponse { // eslint-disable-next-line @typescript-eslint/no-explicit-any export function createOnDataHandler( - stderr: Uint8Array[], - onData: (out: Uint8Array, err: Uint8Array) => void, - onResponse?: (response: R) => void + options: StreamingResponse.Options ): (response: R) => void { return (response: R) => { const out = response.getOutStream_asU8(); const err = response.getErrStream_asU8(); - stderr.push(err); - onData(out, err); - if (onResponse) { - onResponse(response); - } + options.stderr.push(err); + options.onData(out, err); + options.handlers?.forEach((handler) => handler(response)); }; } + export interface Options { + readonly stderr: Uint8Array[]; + readonly onData: (out: Uint8Array, err: Uint8Array) => void; + /** + * Additional request handlers. + * For example, when tracing the progress of a task and + * collecting the output (out, err) and the `build_path` from the CLI. + */ + readonly handlers?: ((response: R) => void)[]; + } } diff --git a/arduino-ide-extension/src/node/grpc-progressible.ts b/arduino-ide-extension/src/node/grpc-progressible.ts index 0a262dd53..c9b93183d 100644 --- a/arduino-ide-extension/src/node/grpc-progressible.ts +++ b/arduino-ide-extension/src/node/grpc-progressible.ts @@ -12,6 +12,7 @@ import { DownloadProgress, TaskProgress, } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; +import { CompileResponse } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb'; import { PlatformInstallResponse, PlatformUninstallResponse, @@ -21,6 +22,11 @@ import { LibraryUninstallResponse, ZipLibraryInstallResponse, } from './cli-protocol/cc/arduino/cli/commands/v1/lib_pb'; +import { + BurnBootloaderResponse, + UploadResponse, + UploadUsingProgrammerResponse, +} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; type LibraryProgressResponse = | LibraryInstallResponse @@ -78,15 +84,62 @@ namespace IndexProgressResponse { return { download: response.getDownloadProgress() }; } } +/** + * These responses have neither `task` nor `progress` property but for the sake of completeness + * on typings (from the gRPC API) and UX, these responses represent an indefinite progress. + */ +type IndefiniteProgressResponse = + | UploadResponse + | UploadUsingProgrammerResponse + | BurnBootloaderResponse; +namespace IndefiniteProgressResponse { + export function is( + response: unknown + ): response is IndefiniteProgressResponse { + return ( + response instanceof UploadResponse || + response instanceof UploadUsingProgrammerResponse || + response instanceof BurnBootloaderResponse + ); + } +} +type DefiniteProgressResponse = CompileResponse; +namespace DefiniteProgressResponse { + export function is(response: unknown): response is DefiniteProgressResponse { + return response instanceof CompileResponse; + } +} +type CoreProgressResponse = + | DefiniteProgressResponse + | IndefiniteProgressResponse; +namespace CoreProgressResponse { + export function is(response: unknown): response is CoreProgressResponse { + return ( + DefiniteProgressResponse.is(response) || + IndefiniteProgressResponse.is(response) + ); + } + export function workUnit(response: CoreProgressResponse): UnitOfWork { + if (DefiniteProgressResponse.is(response)) { + return { task: response.getProgress() }; + } + return UnitOfWork.Unknown; + } +} + export type ProgressResponse = | LibraryProgressResponse | PlatformProgressResponse - | IndexProgressResponse; + | IndexProgressResponse + | CoreProgressResponse; interface UnitOfWork { task?: TaskProgress; download?: DownloadProgress; } +namespace UnitOfWork { + export const Unknown: UnitOfWork = {}; +} /** * It's solely a dev thing. Flip it to `true` if you want to debug the progress from the CLI responses. @@ -115,14 +168,28 @@ export namespace ExecuteWithProgress { console.log(`Progress response [${uuid}]: ${json}`); } } - const { task, download } = resolve(response); + const unitOfWork = resolve(response); + const { task, download } = unitOfWork; if (!download && !task) { - console.warn( - "Implementation error. Neither 'download' nor 'task' is available." - ); - // This is still an API error from the CLI, but IDE2 ignores it. - // Technically, it does not cause an error, but could mess up the progress reporting. - // See an example of an empty object `{}` repose here: https://github.com/arduino/arduino-ide/issues/906#issuecomment-1171145630. + // report a fake unknown progress. + if (unitOfWork === UnitOfWork.Unknown && progressId) { + if (progressId) { + responseService.reportProgress?.({ + progressId, + message: '', + work: { done: Number.NaN, total: Number.NaN }, + }); + } + return; + } + if (DEBUG) { + // This is still an API error from the CLI, but IDE2 ignores it. + // Technically, it does not cause an error, but could mess up the progress reporting. + // See an example of an empty object `{}` repose here: https://github.com/arduino/arduino-ide/issues/906#issuecomment-1171145630. + console.warn( + "Implementation error. Neither 'download' nor 'task' is available." + ); + } return; } if (task && download) { @@ -132,6 +199,7 @@ export namespace ExecuteWithProgress { } if (task) { const message = task.getName() || task.getMessage(); + const percent = task.getPercent(); if (message) { if (progressId) { responseService.reportProgress?.({ @@ -141,6 +209,14 @@ export namespace ExecuteWithProgress { }); } responseService.appendToOutput?.({ chunk: `${message}\n` }); + } else if (percent) { + if (progressId) { + responseService.reportProgress?.({ + progressId, + message, + work: { done: percent, total: 100 }, + }); + } } } else if (download) { if (download.getFile() && !localFile) { @@ -191,38 +267,38 @@ export namespace ExecuteWithProgress { return PlatformProgressResponse.workUnit(response); } else if (IndexProgressResponse.is(response)) { return IndexProgressResponse.workUnit(response); + } else if (CoreProgressResponse.is(response)) { + return CoreProgressResponse.workUnit(response); } console.warn('Unhandled gRPC response', response); return {}; } function toJson(response: ProgressResponse): string | undefined { + let object: Record | undefined = undefined; if (response instanceof LibraryInstallResponse) { - return JSON.stringify(LibraryInstallResponse.toObject(false, response)); + object = LibraryInstallResponse.toObject(false, response); } else if (response instanceof LibraryUninstallResponse) { - return JSON.stringify(LibraryUninstallResponse.toObject(false, response)); + object = LibraryUninstallResponse.toObject(false, response); } else if (response instanceof ZipLibraryInstallResponse) { - return JSON.stringify( - ZipLibraryInstallResponse.toObject(false, response) - ); + object = ZipLibraryInstallResponse.toObject(false, response); } else if (response instanceof PlatformInstallResponse) { - return JSON.stringify(PlatformInstallResponse.toObject(false, response)); + object = PlatformInstallResponse.toObject(false, response); } else if (response instanceof PlatformUninstallResponse) { - return JSON.stringify( - PlatformUninstallResponse.toObject(false, response) - ); + object = PlatformUninstallResponse.toObject(false, response); } else if (response instanceof UpdateIndexResponse) { - return JSON.stringify(UpdateIndexResponse.toObject(false, response)); + object = UpdateIndexResponse.toObject(false, response); } else if (response instanceof UpdateLibrariesIndexResponse) { - return JSON.stringify( - UpdateLibrariesIndexResponse.toObject(false, response) - ); + object = UpdateLibrariesIndexResponse.toObject(false, response); } else if (response instanceof UpdateCoreLibrariesIndexResponse) { - return JSON.stringify( - UpdateCoreLibrariesIndexResponse.toObject(false, response) - ); + object = UpdateCoreLibrariesIndexResponse.toObject(false, response); + } else if (response instanceof CompileResponse) { + object = CompileResponse.toObject(false, response); } - console.warn('Unhandled gRPC response', response); - return undefined; + if (!object) { + console.warn('Unhandled gRPC response', response); + return undefined; + } + return JSON.stringify(object); } } diff --git a/i18n/en.json b/i18n/en.json index 19a3645e0..88c997c90 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -36,6 +36,7 @@ "boardsManager": "Boards Manager", "bootloader": { "burnBootloader": "Burn Bootloader", + "burningBootloader": "Burning bootloader...", "doneBurningBootloader": "Done burning bootloader." }, "burnBootloader": { @@ -306,6 +307,7 @@ "archiveSketch": "Archive Sketch", "cantOpen": "A folder named \"{0}\" already exists. Can't open sketch.", "close": "Are you sure you want to close the sketch?", + "compile": "Compiling sketch...", "configureAndUpload": "Configure And Upload", "createdArchive": "Created archive '{0}'.", "doneCompiling": "Done compiling.", @@ -327,6 +329,7 @@ "titleSketchbook": "Sketchbook", "upload": "Upload", "uploadUsingProgrammer": "Upload Using Programmer", + "uploading": "Uploading...", "userFieldsNotFoundError": "Can't find user fields for connected board", "verify": "Verify", "verifyOrCompile": "Verify/Compile"