From 4810de93bb85a7a8eac12a260ccc58cbafd068df Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Mon, 5 Feb 2024 13:17:00 +0100 Subject: [PATCH] feat: cancelable verify+upload Closes #1199 Signed-off-by: Akos Kitta --- .../src/browser/contributions/contribution.ts | 124 ++++++---- .../browser/contributions/upload-sketch.ts | 5 +- .../browser/contributions/verify-sketch.ts | 26 +- .../browser/library/library-list-widget.ts | 6 +- .../component-list/component-list-item.tsx | 2 +- .../filterable-list-container.tsx | 7 +- .../widgets/component-list/list-widget.tsx | 7 - arduino-ide-extension/src/common/nls.ts | 2 + .../src/common/protocol/core-service.ts | 18 +- .../src/common/protocol/progressible.ts | 43 +++- .../src/node/core-service-impl.ts | 230 +++++++++++------- i18n/en.json | 3 +- 12 files changed, 298 insertions(+), 175 deletions(-) diff --git a/arduino-ide-extension/src/browser/contributions/contribution.ts b/arduino-ide-extension/src/browser/contributions/contribution.ts index 8de4d8ff4..1a8ff5798 100644 --- a/arduino-ide-extension/src/browser/contributions/contribution.ts +++ b/arduino-ide-extension/src/browser/contributions/contribution.ts @@ -1,83 +1,89 @@ +import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; import { - inject, - injectable, - interfaces, - postConstruct, -} from '@theia/core/shared/inversify'; -import URI from '@theia/core/lib/common/uri'; -import { ILogger } from '@theia/core/lib/common/logger'; -import { - Disposable, - DisposableCollection, -} from '@theia/core/lib/common/disposable'; -import { Saveable } from '@theia/core/lib/browser/saveable'; -import { FileService } from '@theia/filesystem/lib/browser/file-service'; -import { MaybePromise } from '@theia/core/lib/common/types'; -import { LabelProvider } from '@theia/core/lib/browser/label-provider'; -import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; -import { MessageService } from '@theia/core/lib/common/message-service'; -import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; -import { open, OpenerService } from '@theia/core/lib/browser/opener-service'; -import { - MenuModelRegistry, - MenuContribution, -} from '@theia/core/lib/common/menu'; + FrontendApplication, + FrontendApplicationContribution, +} from '@theia/core/lib/browser/frontend-application'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { - KeybindingRegistry, KeybindingContribution, + KeybindingRegistry, } from '@theia/core/lib/browser/keybinding'; +import { LabelProvider } from '@theia/core/lib/browser/label-provider'; +import { OpenerService, open } from '@theia/core/lib/browser/opener-service'; +import { Saveable } from '@theia/core/lib/browser/saveable'; +import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { TabBarToolbarContribution, TabBarToolbarRegistry, } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; -import { - FrontendApplicationContribution, - FrontendApplication, -} from '@theia/core/lib/browser/frontend-application'; +import { CancellationToken } from '@theia/core/lib/common/cancellation'; import { Command, - CommandRegistry, CommandContribution, + CommandRegistry, CommandService, } from '@theia/core/lib/common/command'; -import { SettingsService } from '../dialogs/settings/settings'; import { - CurrentSketch, - SketchesServiceClientImpl, -} from '../sketches-service-client-impl'; + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { ILogger } from '@theia/core/lib/common/logger'; +import { + MenuContribution, + MenuModelRegistry, +} from '@theia/core/lib/common/menu'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { MessageType } from '@theia/core/lib/common/message-service-protocol'; +import { nls } from '@theia/core/lib/common/nls'; +import { MaybePromise, isObject } from '@theia/core/lib/common/types'; +import URI from '@theia/core/lib/common/uri'; +import { + inject, + injectable, + interfaces, + postConstruct, +} from '@theia/core/shared/inversify'; +import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager'; +import { OutputChannelSeverity } from '@theia/output/lib/browser/output-channel'; +import { MainMenuManager } from '../../common/main-menu-manager'; +import { userAbort } from '../../common/nls'; import { - SketchesService, - FileSystemExt, - Sketch, - CoreService, CoreError, + CoreService, + FileSystemExt, ResponseServiceClient, + Sketch, + SketchesService, } from '../../common/protocol'; +import { + ExecuteWithProgress, + UserAbortApplicationError, +} from '../../common/protocol/progressible'; import { ArduinoPreferences } from '../arduino-preferences'; -import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; -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'; -import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager'; -import { MessageType } from '@theia/core/lib/common/message-service-protocol'; -import { WorkspaceService } from '../theia/workspace/workspace-service'; -import { MainMenuManager } from '../../common/main-menu-manager'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { ConfigServiceClient } from '../config/config-service-client'; -import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { DialogService } from '../dialog-service'; +import { SettingsService } from '../dialogs/settings/settings'; +import { + CurrentSketch, + SketchesServiceClientImpl, +} from '../sketches-service-client-impl'; import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service'; +import { OutputChannelManager } from '../theia/output/output-channel'; +import { WorkspaceService } from '../theia/workspace/workspace-service'; export { Command, CommandRegistry, - MenuModelRegistry, KeybindingRegistry, + MenuModelRegistry, + Sketch, TabBarToolbarRegistry, URI, - Sketch, open, }; @@ -247,6 +253,12 @@ export abstract class CoreServiceContribution extends SketchContribution { } protected handleError(error: unknown): void { + if (isObject(error) && UserAbortApplicationError.is(error)) { + this.outputChannelManager + .getChannel('Arduino') + .appendLine(userAbort, OutputChannelSeverity.Warning); + return; + } this.tryToastErrorMessage(error); } @@ -293,7 +305,13 @@ export abstract class CoreServiceContribution extends SketchContribution { protected async doWithProgress(options: { progressText: string; keepOutput?: boolean; - task: (progressId: string, coreService: CoreService) => Promise; + task: ( + progressId: string, + coreService: CoreService, + cancellationToken?: CancellationToken + ) => Promise; + // false by default + cancelable?: boolean; }): Promise { const toDisposeOnComplete = new DisposableCollection( this.maybeActivateMonitorWidget() @@ -306,8 +324,10 @@ export abstract class CoreServiceContribution extends SketchContribution { messageService: this.messageService, responseService: this.responseService, progressText, - run: ({ progressId }) => task(progressId, this.coreService), + run: ({ progressId, cancellationToken }) => + task(progressId, this.coreService, cancellationToken), keepOutput, + cancelable: options.cancelable, }); toDisposeOnComplete.dispose(); return result; diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index 75c591dba..d6745d232 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -136,9 +136,10 @@ export class UploadSketch extends CoreServiceContribution { const uploadResponse = await this.doWithProgress({ progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'), - task: (progressId, coreService) => - coreService.upload({ ...uploadOptions, progressId }), + task: (progressId, coreService, token) => + coreService.upload({ ...uploadOptions, progressId }, token), keepOutput: true, + cancelable: true, }); // the port update is NOOP if nothing has changed this.boardsServiceProvider.updateConfig(uploadResponse.portAfterUpload); diff --git a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts index bd6b60ff8..999c3ec5c 100644 --- a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts @@ -1,18 +1,18 @@ -import { inject, injectable } from '@theia/core/shared/inversify'; import { Emitter } from '@theia/core/lib/common/event'; +import { nls } from '@theia/core/lib/common/nls'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import type { CoreService } from '../../common/protocol'; import { ArduinoMenus } from '../menu/arduino-menus'; +import { CurrentSketch } from '../sketches-service-client-impl'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { - CoreServiceContribution, Command, CommandRegistry, - MenuModelRegistry, + CoreServiceContribution, KeybindingRegistry, + MenuModelRegistry, TabBarToolbarRegistry, } from './contribution'; -import { nls } from '@theia/core/lib/common'; -import { CurrentSketch } from '../sketches-service-client-impl'; -import { CoreService } from '../../common/protocol'; import { CoreErrorHandler } from './core-error-handler'; export interface VerifySketchParams { @@ -131,11 +131,15 @@ export class VerifySketch extends CoreServiceContribution { 'arduino/sketch/compile', 'Compiling sketch...' ), - task: (progressId, coreService) => - coreService.compile({ - ...options, - progressId, - }), + task: (progressId, coreService, token) => + coreService.compile( + { + ...options, + progressId, + }, + token + ), + cancelable: true, }); this.messageService.info( nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'), diff --git a/arduino-ide-extension/src/browser/library/library-list-widget.ts b/arduino-ide-extension/src/browser/library/library-list-widget.ts index 690799923..660bcec06 100644 --- a/arduino-ide-extension/src/browser/library/library-list-widget.ts +++ b/arduino-ide-extension/src/browser/library/library-list-widget.ts @@ -12,15 +12,13 @@ import { LibrarySearch, LibraryService, } from '../../common/protocol/library-service'; -import { - ListWidget, - UserAbortError, -} from '../widgets/component-list/list-widget'; +import { ListWidget } from '../widgets/component-list/list-widget'; import { Installable } from '../../common/protocol'; import { ListItemRenderer } from '../widgets/component-list/list-item-renderer'; import { nls } from '@theia/core/lib/common'; import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer'; import { findChildTheiaButton, splitByBoldTag } from '../utils/dom'; +import { UserAbortError } from '../../common/protocol/progressible'; @injectable() export class LibraryListWidget extends ListWidget< diff --git a/arduino-ide-extension/src/browser/widgets/component-list/component-list-item.tsx b/arduino-ide-extension/src/browser/widgets/component-list/component-list-item.tsx index 1e13b1417..d5e320ee1 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/component-list-item.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/component-list-item.tsx @@ -2,7 +2,7 @@ import React from '@theia/core/shared/react'; import type { ArduinoComponent } from '../../../common/protocol/arduino-component'; import { Installable } from '../../../common/protocol/installable'; import type { ListItemRenderer } from './list-item-renderer'; -import { UserAbortError } from './list-widget'; +import { UserAbortError } from '../../../common/protocol/progressible'; export class ComponentListItem< T extends ArduinoComponent 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 0039d76d1..086ffbf4f 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,7 +5,10 @@ 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 { + ExecuteWithProgress, + UserAbortError, +} from '../../../common/protocol/progressible'; import { Installable, libraryInstallFailed, @@ -13,7 +16,7 @@ import { } from '../../../common/protocol/installable'; import { ArduinoComponent } from '../../../common/protocol/arduino-component'; import { SearchBar } from './search-bar'; -import { ListWidget, UserAbortError } from './list-widget'; +import { ListWidget } from './list-widget'; import { ComponentList } from './component-list'; import { ListItemRenderer } from './list-item-renderer'; import { diff --git a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx index 7603f44c0..d13d4ac7d 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx @@ -192,10 +192,3 @@ export namespace ListWidget { readonly defaultSearchOptions: S; } } - -export class UserAbortError extends Error { - constructor(message = 'User abort') { - super(message); - Object.setPrototypeOf(this, UserAbortError.prototype); - } -} diff --git a/arduino-ide-extension/src/common/nls.ts b/arduino-ide-extension/src/common/nls.ts index d824f1751..7514a45c8 100644 --- a/arduino-ide-extension/src/common/nls.ts +++ b/arduino-ide-extension/src/common/nls.ts @@ -39,3 +39,5 @@ export const noSketchOpened = nls.localize( 'arduino/common/noSketchOpened', 'No sketch opened' ); + +export const userAbort = nls.localize('arduino/common/userAbort', 'User abort'); diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index 9104df01e..f3a681d48 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -1,4 +1,5 @@ import { ApplicationError } from '@theia/core/lib/common/application-error'; +import type { CancellationToken } from '@theia/core/lib/common/cancellation'; import { nls } from '@theia/core/lib/common/nls'; import type { Location, @@ -7,7 +8,7 @@ import type { } from '@theia/core/shared/vscode-languageserver-protocol'; import type { CompileSummary as ApiCompileSummary } from 'vscode-arduino-api'; import type { BoardUserField, Installable } from '../../common/protocol/'; -import { isPortIdentifier, PortIdentifier, Programmer } from './boards-service'; +import { PortIdentifier, Programmer, isPortIdentifier } from './boards-service'; import type { IndexUpdateSummary } from './notification-service'; import type { Sketch } from './sketches-service'; @@ -162,9 +163,18 @@ export function isUploadResponse(arg: unknown): arg is UploadResponse { export const CoreServicePath = '/services/core-service'; export const CoreService = Symbol('CoreService'); export interface CoreService { - compile(options: CoreService.Options.Compile): Promise; - upload(options: CoreService.Options.Upload): Promise; - burnBootloader(options: CoreService.Options.Bootloader): Promise; + compile( + options: CoreService.Options.Compile, + cancellationToken?: CancellationToken + ): Promise; + upload( + options: CoreService.Options.Upload, + cancellationToken?: CancellationToken + ): Promise; + burnBootloader( + options: CoreService.Options.Bootloader, + cancellationToken?: CancellationToken + ): Promise; /** * Refreshes the underling core gRPC client for the Arduino CLI. */ diff --git a/arduino-ide-extension/src/common/protocol/progressible.ts b/arduino-ide-extension/src/common/protocol/progressible.ts index c27737ccc..d01bc270d 100644 --- a/arduino-ide-extension/src/common/protocol/progressible.ts +++ b/arduino-ide-extension/src/common/protocol/progressible.ts @@ -1,22 +1,48 @@ +import { ApplicationError } from '@theia/core/lib/common/application-error'; 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 { userAbort } from '../nls'; import type { ResponseServiceClient } from './response-service'; +export const UserAbortApplicationError = ApplicationError.declare( + 9999, + (message: string, uri: string) => { + return { + message, + data: { uri }, + }; + } +); + +export class UserAbortError extends Error { + constructor() { + super(userAbort); + Object.setPrototypeOf(this, UserAbortError.prototype); + } +} + export namespace ExecuteWithProgress { export async function doWithProgress(options: { - run: ({ progressId }: { progressId: string }) => Promise; + run: ({ + progressId, + cancellationToken, + }: { + progressId: string; + cancellationToken?: CancellationToken; + }) => Promise; messageService: MessageService; responseService: ResponseServiceClient; progressText: string; keepOutput?: boolean; + cancelable?: boolean; }): Promise { return withProgress( options.progressText, options.messageService, // eslint-disable-next-line @typescript-eslint/no-unused-vars - async (progress, _token) => { + async (progress, token) => { const progressId = progress.id; const toDispose = options.responseService.onProgressDidChange( (progressMessage) => { @@ -30,24 +56,29 @@ export namespace ExecuteWithProgress { if (!options.keepOutput) { options.responseService.clearOutput(); } - const result = await options.run({ progressId }); + const result = await options.run({ + progressId, + cancellationToken: token, + }); return result; } finally { toDispose.dispose(); } - } + }, + options.cancelable ); } export async function withProgress( text: string, messageService: MessageService, - cb: (progress: Progress, token: CancellationToken) => Promise + cb: (progress: Progress, token: CancellationToken) => Promise, + cancelable = false ): Promise { const cancellationSource = new CancellationTokenSource(); const { token } = cancellationSource; const progress = await messageService.showProgress( - { text, options: { cancelable: false } }, + { text, options: { cancelable } }, () => cancellationSource.cancel() ); try { diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index 3a9677c18..9cb1c4584 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -1,22 +1,44 @@ +import type { ClientReadableStream } from '@grpc/grpc-js'; +import { ApplicationError } from '@theia/core/lib/common/application-error'; +import type { CancellationToken } from '@theia/core/lib/common/cancellation'; +import { CommandService } from '@theia/core/lib/common/command'; +import { + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; +import { nls } from '@theia/core/lib/common/nls'; +import type { Mutable } from '@theia/core/lib/common/types'; import { FileUri } from '@theia/core/lib/node/file-uri'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { relative } from 'node:path'; import * as jspb from 'google-protobuf'; import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb'; -import type { ClientReadableStream } from '@grpc/grpc-js'; +import path from 'node:path'; import { + UploadResponse as ApiUploadResponse, + OutputMessage, + Port, + PortIdentifier, + resolveDetectedPort, +} from '../common/protocol'; +import { + CompileSummary, CompilerWarnings, - CoreService, CoreError, - CompileSummary, + CoreService, isCompileSummary, isUploadResponse, } from '../common/protocol/core-service'; +import { ResponseService } from '../common/protocol/response-service'; +import { firstToUpperCase, notEmpty } from '../common/utils'; +import { BoardDiscovery, createApiPort } from './board-discovery'; +import { tryParseError } from './cli-error-parser'; +import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb'; +import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; import { CompileRequest, CompileResponse, } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb'; -import { CoreClientAware } from './core-client-provider'; +import { Port as RpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb'; import { BurnBootloaderRequest, BurnBootloaderResponse, @@ -25,26 +47,13 @@ import { UploadUsingProgrammerRequest, UploadUsingProgrammerResponse, } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; -import { ResponseService } from '../common/protocol/response-service'; -import { - resolveDetectedPort, - OutputMessage, - PortIdentifier, - Port, - UploadResponse as ApiUploadResponse, -} from '../common/protocol'; -import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb'; -import { Port as RpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb'; -import { ApplicationError, CommandService, Disposable, nls } from '@theia/core'; +import { CoreClientAware } from './core-client-provider'; +import { ExecuteWithProgress, ProgressResponse } from './grpc-progressible'; import { MonitorManager } from './monitor-manager'; -import { AutoFlushingBuffer } from './utils/buffers'; -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'; -import type { Mutable } from '@theia/core/lib/common/types'; -import { BoardDiscovery, createApiPort } from './board-discovery'; +import { AutoFlushingBuffer } from './utils/buffers'; +import { userAbort } from '../common/nls'; +import { UserAbortApplicationError } from '../common/protocol/progressible'; namespace Uploadable { export type Request = UploadRequest | UploadUsingProgrammerRequest; @@ -64,9 +73,13 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { @inject(BoardDiscovery) private readonly boardDiscovery: BoardDiscovery; - async compile(options: CoreService.Options.Compile): Promise { + async compile( + options: CoreService.Options.Compile, + cancellationToken?: CancellationToken + ): Promise { const coreClient = await this.coreClient; const { client, instance } = coreClient; + const request = this.compileRequest(options, instance); const compileSummary = {}; const progressHandler = this.createProgressHandler(options); const compileSummaryHandler = (response: CompileResponse) => @@ -75,10 +88,15 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { progressHandler, compileSummaryHandler ); - const request = this.compileRequest(options, instance); + const toDisposeOnFinally = new DisposableCollection(handler); return new Promise((resolve, reject) => { - client - .compile(request) + const call = client.compile(request); + if (cancellationToken) { + toDisposeOnFinally.push( + cancellationToken.onCancellationRequested(() => call.cancel()) + ); + } + call .on('data', handler.onData) .on('error', (error) => { if (!ServiceError.is(error)) { @@ -87,30 +105,39 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { error ); reject(error); - } else { - const compilerErrors = tryParseError({ - content: handler.content, - sketch: options.sketch, - }); - const message = nls.localize( - 'arduino/compile/error', - 'Compilation error: {0}', - compilerErrors - .map(({ message }) => message) - .filter(notEmpty) - .shift() ?? error.details - ); - this.sendResponse( - error.details + '\n\n' + message, - OutputMessage.Severity.Error - ); - reject(CoreError.VerifyFailed(message, compilerErrors)); + return; } + if (ServiceError.isCancel(error)) { + console.log(userAbort); + reject(UserAbortApplicationError()); + return; + } + const compilerErrors = tryParseError({ + content: handler.content, + sketch: options.sketch, + }); + const message = nls.localize( + 'arduino/compile/error', + 'Compilation error: {0}', + compilerErrors + .map(({ message }) => message) + .filter(notEmpty) + .shift() ?? error.details + ); + this.sendResponse( + error.details + '\n\n' + message, + OutputMessage.Severity.Error + ); + reject(CoreError.VerifyFailed(message, compilerErrors)); }) .on('end', resolve); }).finally(() => { - handler.dispose(); + toDisposeOnFinally.dispose(); if (!isCompileSummary(compileSummary)) { + if (cancellationToken && cancellationToken.isCancellationRequested) { + // NOOP + return; + } console.error( `Have not received the full compile summary from the CLI while running the compilation. ${JSON.stringify( compileSummary @@ -176,7 +203,10 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { return request; } - upload(options: CoreService.Options.Upload): Promise { + upload( + options: CoreService.Options.Upload, + cancellationToken?: CancellationToken + ): Promise { const { usingProgrammer } = options; return this.doUpload( options, @@ -190,7 +220,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { usingProgrammer ? CoreError.UploadUsingProgrammerFailed : CoreError.UploadFailed, - `upload${usingProgrammer ? ' using programmer' : ''}` + `upload${usingProgrammer ? ' using programmer' : ''}`, + cancellationToken ); } @@ -204,7 +235,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { client: ArduinoCoreServiceClient ) => (request: REQ) => ClientReadableStream, errorCtor: ApplicationError.Constructor, - task: string + task: string, + cancellationToken?: CancellationToken ): Promise { const portBeforeUpload = options.port; const uploadResponseFragment: Mutable> = { @@ -241,33 +273,47 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { progressHandler, updateUploadResponseFragmentHandler ); + const toDisposeOnFinally = new DisposableCollection(handler); const grpcCall = responseFactory(client); return this.notifyUploadWillStart(options).then(() => new Promise((resolve, reject) => { - grpcCall(this.initUploadRequest(request, options, instance)) + const call = grpcCall( + this.initUploadRequest(request, options, instance) + ); + if (cancellationToken) { + toDisposeOnFinally.push( + cancellationToken.onCancellationRequested(() => call.cancel()) + ); + } + call .on('data', handler.onData) .on('error', (error) => { if (!ServiceError.is(error)) { console.error(`Unexpected error occurred while ${task}.`, error); reject(error); - } else { - const message = nls.localize( - 'arduino/upload/error', - '{0} error: {1}', - firstToUpperCase(task), - error.details - ); - this.sendResponse(error.details, OutputMessage.Severity.Error); - reject( - errorCtor( - message, - tryParseError({ - content: handler.content, - sketch: options.sketch, - }) - ) - ); + return; + } + if (ServiceError.isCancel(error)) { + console.log(userAbort); + reject(UserAbortApplicationError()); + return; } + const message = nls.localize( + 'arduino/upload/error', + '{0} error: {1}', + firstToUpperCase(task), + error.details + ); + this.sendResponse(error.details, OutputMessage.Severity.Error); + reject( + errorCtor( + message, + tryParseError({ + content: handler.content, + sketch: options.sketch, + }) + ) + ); }) .on('end', () => { if (isUploadResponse(uploadResponseFragment)) { @@ -285,7 +331,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { } }); }).finally(async () => { - handler.dispose(); + toDisposeOnFinally.dispose(); await this.notifyUploadDidFinish( Object.assign(options, { afterPort: uploadResponseFragment.portAfterUpload, @@ -320,16 +366,25 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { return request; } - async burnBootloader(options: CoreService.Options.Bootloader): Promise { + async burnBootloader( + options: CoreService.Options.Bootloader, + cancellationToken?: CancellationToken + ): Promise { const coreClient = await this.coreClient; const { client, instance } = coreClient; const progressHandler = this.createProgressHandler(options); const handler = this.createOnDataHandler(progressHandler); const request = this.burnBootloaderRequest(options, instance); + const toDisposeOnFinally = new DisposableCollection(handler); return this.notifyUploadWillStart(options).then(() => new Promise((resolve, reject) => { - client - .burnBootloader(request) + const call = client.burnBootloader(request); + if (cancellationToken) { + toDisposeOnFinally.push( + cancellationToken.onCancellationRequested(() => call.cancel()) + ); + } + call .on('data', handler.onData) .on('error', (error) => { if (!ServiceError.is(error)) { @@ -338,23 +393,28 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { error ); reject(error); - } else { - this.sendResponse(error.details, OutputMessage.Severity.Error); - reject( - CoreError.BurnBootloaderFailed( - nls.localize( - 'arduino/burnBootloader/error', - 'Error while burning the bootloader: {0}', - error.details - ), - tryParseError({ content: handler.content }) - ) - ); + return; } + if (ServiceError.isCancel(error)) { + console.log(userAbort); + reject(UserAbortApplicationError()); + return; + } + this.sendResponse(error.details, OutputMessage.Severity.Error); + reject( + CoreError.BurnBootloaderFailed( + nls.localize( + 'arduino/burnBootloader/error', + 'Error while burning the bootloader: {0}', + error.details + ), + tryParseError({ content: handler.content }) + ) + ); }) .on('end', resolve); }).finally(async () => { - handler.dispose(); + toDisposeOnFinally.dispose(); await this.notifyUploadDidFinish( Object.assign(options, { afterPort: options.port }) ); @@ -463,7 +523,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { for (const uri of Object.keys(options.sourceOverride)) { const content = options.sourceOverride[uri]; if (content) { - const relativePath = relative(sketchPath, FileUri.fsPath(uri)); + const relativePath = path.relative(sketchPath, FileUri.fsPath(uri)); req.getSourceOverrideMap().set(relativePath, content); } } diff --git a/i18n/en.json b/i18n/en.json index 841ab3a91..8d5ee8db9 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -152,7 +152,8 @@ "serialMonitor": "Serial Monitor", "type": "Type", "unknown": "Unknown", - "updateable": "Updatable" + "updateable": "Updatable", + "userAbort": "User abort" }, "compile": { "error": "Compilation error: {0}"