feat: cancelable verify+upload

Closes arduino/arduino-ide#1199

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2024-02-05 13:17:00 +01:00 committed by Akos Kitta
parent 347e3d8118
commit 2a325a5b74
13 changed files with 317 additions and 190 deletions

View File

@ -37,11 +37,15 @@ export class BurnBootloader extends CoreServiceContribution {
'arduino/bootloader/burningBootloader', 'arduino/bootloader/burningBootloader',
'Burning bootloader...' 'Burning bootloader...'
), ),
task: (progressId, coreService) => task: (progressId, coreService, token) =>
coreService.burnBootloader({ coreService.burnBootloader(
{
...options, ...options,
progressId, progressId,
}), },
token
),
cancelable: true,
}); });
this.messageService.info( this.messageService.info(
nls.localize( nls.localize(

View File

@ -1,83 +1,89 @@
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import {
FrontendApplication,
FrontendApplicationContribution,
} from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
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 { CancellationToken } from '@theia/core/lib/common/cancellation';
import {
Command,
CommandContribution,
CommandRegistry,
CommandService,
} from '@theia/core/lib/common/command';
import {
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 { import {
inject, inject,
injectable, injectable,
interfaces, interfaces,
postConstruct, postConstruct,
} from '@theia/core/shared/inversify'; } 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 { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service'; import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service'; import { OutputChannelSeverity } from '@theia/output/lib/browser/output-channel';
import { MainMenuManager } from '../../common/main-menu-manager';
import { userAbort } from '../../common/nls';
import { import {
MenuModelRegistry, CoreError,
MenuContribution, CoreService,
} from '@theia/core/lib/common/menu'; FileSystemExt,
ResponseServiceClient,
Sketch,
SketchesService,
} from '../../common/protocol';
import { import {
KeybindingRegistry, ExecuteWithProgress,
KeybindingContribution, UserAbortApplicationError,
} from '@theia/core/lib/browser/keybinding'; } from '../../common/protocol/progressible';
import { import { ArduinoPreferences } from '../arduino-preferences';
TabBarToolbarContribution, import { BoardsDataStore } from '../boards/boards-data-store';
TabBarToolbarRegistry, import { BoardsServiceProvider } from '../boards/boards-service-provider';
} from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { ConfigServiceClient } from '../config/config-service-client';
import { import { DialogService } from '../dialog-service';
FrontendApplicationContribution,
FrontendApplication,
} from '@theia/core/lib/browser/frontend-application';
import {
Command,
CommandRegistry,
CommandContribution,
CommandService,
} from '@theia/core/lib/common/command';
import { SettingsService } from '../dialogs/settings/settings'; import { SettingsService } from '../dialogs/settings/settings';
import { import {
CurrentSketch, CurrentSketch,
SketchesServiceClientImpl, SketchesServiceClientImpl,
} from '../sketches-service-client-impl'; } from '../sketches-service-client-impl';
import {
SketchesService,
FileSystemExt,
Sketch,
CoreService,
CoreError,
ResponseServiceClient,
} from '../../common/protocol';
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 { ConfigServiceClient } from '../config/config-service-client';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { DialogService } from '../dialog-service';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service'; import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
import { OutputChannelManager } from '../theia/output/output-channel';
import { WorkspaceService } from '../theia/workspace/workspace-service';
export { export {
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
MenuModelRegistry,
Sketch,
TabBarToolbarRegistry, TabBarToolbarRegistry,
URI, URI,
Sketch,
open, open,
}; };
@ -247,6 +253,12 @@ export abstract class CoreServiceContribution extends SketchContribution {
} }
protected handleError(error: unknown): void { protected handleError(error: unknown): void {
if (isObject(error) && UserAbortApplicationError.is(error)) {
this.outputChannelManager
.getChannel('Arduino')
.appendLine(userAbort, OutputChannelSeverity.Warning);
return;
}
this.tryToastErrorMessage(error); this.tryToastErrorMessage(error);
} }
@ -293,7 +305,13 @@ export abstract class CoreServiceContribution extends SketchContribution {
protected async doWithProgress<T>(options: { protected async doWithProgress<T>(options: {
progressText: string; progressText: string;
keepOutput?: boolean; keepOutput?: boolean;
task: (progressId: string, coreService: CoreService) => Promise<T>; task: (
progressId: string,
coreService: CoreService,
cancellationToken?: CancellationToken
) => Promise<T>;
// false by default
cancelable?: boolean;
}): Promise<T> { }): Promise<T> {
const toDisposeOnComplete = new DisposableCollection( const toDisposeOnComplete = new DisposableCollection(
this.maybeActivateMonitorWidget() this.maybeActivateMonitorWidget()
@ -306,8 +324,10 @@ export abstract class CoreServiceContribution extends SketchContribution {
messageService: this.messageService, messageService: this.messageService,
responseService: this.responseService, responseService: this.responseService,
progressText, progressText,
run: ({ progressId }) => task(progressId, this.coreService), run: ({ progressId, cancellationToken }) =>
task(progressId, this.coreService, cancellationToken),
keepOutput, keepOutput,
cancelable: options.cancelable,
}); });
toDisposeOnComplete.dispose(); toDisposeOnComplete.dispose();
return result; return result;

View File

@ -136,9 +136,10 @@ export class UploadSketch extends CoreServiceContribution {
const uploadResponse = await this.doWithProgress({ const uploadResponse = await this.doWithProgress({
progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'), progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'),
task: (progressId, coreService) => task: (progressId, coreService, token) =>
coreService.upload({ ...uploadOptions, progressId }), coreService.upload({ ...uploadOptions, progressId }, token),
keepOutput: true, keepOutput: true,
cancelable: true,
}); });
// the port update is NOOP if nothing has changed // the port update is NOOP if nothing has changed
this.boardsServiceProvider.updateConfig(uploadResponse.portAfterUpload); this.boardsServiceProvider.updateConfig(uploadResponse.portAfterUpload);

View File

@ -1,18 +1,18 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event'; 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 { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { import {
CoreServiceContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, CoreServiceContribution,
KeybindingRegistry, KeybindingRegistry,
MenuModelRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } 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'; import { CoreErrorHandler } from './core-error-handler';
export interface VerifySketchParams { export interface VerifySketchParams {
@ -131,11 +131,15 @@ export class VerifySketch extends CoreServiceContribution {
'arduino/sketch/compile', 'arduino/sketch/compile',
'Compiling sketch...' 'Compiling sketch...'
), ),
task: (progressId, coreService) => task: (progressId, coreService, token) =>
coreService.compile({ coreService.compile(
{
...options, ...options,
progressId, progressId,
}), },
token
),
cancelable: true,
}); });
this.messageService.info( this.messageService.info(
nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'), nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'),

View File

@ -12,15 +12,13 @@ import {
LibrarySearch, LibrarySearch,
LibraryService, LibraryService,
} from '../../common/protocol/library-service'; } from '../../common/protocol/library-service';
import { import { ListWidget } from '../widgets/component-list/list-widget';
ListWidget,
UserAbortError,
} from '../widgets/component-list/list-widget';
import { Installable } from '../../common/protocol'; import { Installable } from '../../common/protocol';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer'; import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer'; import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer';
import { findChildTheiaButton, splitByBoldTag } from '../utils/dom'; import { findChildTheiaButton, splitByBoldTag } from '../utils/dom';
import { UserAbortError } from '../../common/protocol/progressible';
@injectable() @injectable()
export class LibraryListWidget extends ListWidget< export class LibraryListWidget extends ListWidget<

View File

@ -2,7 +2,7 @@ import React from '@theia/core/shared/react';
import type { ArduinoComponent } from '../../../common/protocol/arduino-component'; import type { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { Installable } from '../../../common/protocol/installable'; import { Installable } from '../../../common/protocol/installable';
import type { ListItemRenderer } from './list-item-renderer'; import type { ListItemRenderer } from './list-item-renderer';
import { UserAbortError } from './list-widget'; import { UserAbortError } from '../../../common/protocol/progressible';
export class ComponentListItem< export class ComponentListItem<
T extends ArduinoComponent T extends ArduinoComponent

View File

@ -5,7 +5,10 @@ import { CommandService } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { Searchable } from '../../../common/protocol/searchable'; import { Searchable } from '../../../common/protocol/searchable';
import { ExecuteWithProgress } from '../../../common/protocol/progressible'; import {
ExecuteWithProgress,
UserAbortError,
} from '../../../common/protocol/progressible';
import { import {
Installable, Installable,
libraryInstallFailed, libraryInstallFailed,
@ -13,7 +16,7 @@ import {
} from '../../../common/protocol/installable'; } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component'; import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { SearchBar } from './search-bar'; import { SearchBar } from './search-bar';
import { ListWidget, UserAbortError } from './list-widget'; import { ListWidget } from './list-widget';
import { ComponentList } from './component-list'; import { ComponentList } from './component-list';
import { ListItemRenderer } from './list-item-renderer'; import { ListItemRenderer } from './list-item-renderer';
import { import {

View File

@ -192,10 +192,3 @@ export namespace ListWidget {
readonly defaultSearchOptions: S; readonly defaultSearchOptions: S;
} }
} }
export class UserAbortError extends Error {
constructor(message = 'User abort') {
super(message);
Object.setPrototypeOf(this, UserAbortError.prototype);
}
}

View File

@ -39,3 +39,5 @@ export const noSketchOpened = nls.localize(
'arduino/common/noSketchOpened', 'arduino/common/noSketchOpened',
'No sketch opened' 'No sketch opened'
); );
export const userAbort = nls.localize('arduino/common/userAbort', 'User abort');

View File

@ -1,4 +1,5 @@
import { ApplicationError } from '@theia/core/lib/common/application-error'; 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 { nls } from '@theia/core/lib/common/nls';
import type { import type {
Location, Location,
@ -7,7 +8,7 @@ import type {
} from '@theia/core/shared/vscode-languageserver-protocol'; } from '@theia/core/shared/vscode-languageserver-protocol';
import type { CompileSummary as ApiCompileSummary } from 'vscode-arduino-api'; import type { CompileSummary as ApiCompileSummary } from 'vscode-arduino-api';
import type { BoardUserField, Installable } from '../../common/protocol/'; 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 { IndexUpdateSummary } from './notification-service';
import type { Sketch } from './sketches-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 CoreServicePath = '/services/core-service';
export const CoreService = Symbol('CoreService'); export const CoreService = Symbol('CoreService');
export interface CoreService { export interface CoreService {
compile(options: CoreService.Options.Compile): Promise<void>; compile(
upload(options: CoreService.Options.Upload): Promise<UploadResponse>; options: CoreService.Options.Compile,
burnBootloader(options: CoreService.Options.Bootloader): Promise<void>; cancellationToken?: CancellationToken
): Promise<void>;
upload(
options: CoreService.Options.Upload,
cancellationToken?: CancellationToken
): Promise<UploadResponse>;
burnBootloader(
options: CoreService.Options.Bootloader,
cancellationToken?: CancellationToken
): Promise<void>;
/** /**
* Refreshes the underling core gRPC client for the Arduino CLI. * Refreshes the underling core gRPC client for the Arduino CLI.
*/ */

View File

@ -1,22 +1,48 @@
import { ApplicationError } from '@theia/core/lib/common/application-error';
import type { CancellationToken } from '@theia/core/lib/common/cancellation'; import type { CancellationToken } from '@theia/core/lib/common/cancellation';
import { CancellationTokenSource } 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 { MessageService } from '@theia/core/lib/common/message-service';
import type { Progress } from '@theia/core/lib/common/message-service-protocol'; import type { Progress } from '@theia/core/lib/common/message-service-protocol';
import { userAbort } from '../nls';
import type { ResponseServiceClient } from './response-service'; 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 namespace ExecuteWithProgress {
export async function doWithProgress<T>(options: { export async function doWithProgress<T>(options: {
run: ({ progressId }: { progressId: string }) => Promise<T>; run: ({
progressId,
cancellationToken,
}: {
progressId: string;
cancellationToken?: CancellationToken;
}) => Promise<T>;
messageService: MessageService; messageService: MessageService;
responseService: ResponseServiceClient; responseService: ResponseServiceClient;
progressText: string; progressText: string;
keepOutput?: boolean; keepOutput?: boolean;
cancelable?: boolean;
}): Promise<T> { }): Promise<T> {
return withProgress( return withProgress(
options.progressText, options.progressText,
options.messageService, options.messageService,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
async (progress, _token) => { async (progress, token) => {
const progressId = progress.id; const progressId = progress.id;
const toDispose = options.responseService.onProgressDidChange( const toDispose = options.responseService.onProgressDidChange(
(progressMessage) => { (progressMessage) => {
@ -30,24 +56,29 @@ export namespace ExecuteWithProgress {
if (!options.keepOutput) { if (!options.keepOutput) {
options.responseService.clearOutput(); options.responseService.clearOutput();
} }
const result = await options.run({ progressId }); const result = await options.run({
progressId,
cancellationToken: token,
});
return result; return result;
} finally { } finally {
toDispose.dispose(); toDispose.dispose();
} }
} },
options.cancelable
); );
} }
export async function withProgress<T>( export async function withProgress<T>(
text: string, text: string,
messageService: MessageService, messageService: MessageService,
cb: (progress: Progress, token: CancellationToken) => Promise<T> cb: (progress: Progress, token: CancellationToken) => Promise<T>,
cancelable = false
): Promise<T> { ): Promise<T> {
const cancellationSource = new CancellationTokenSource(); const cancellationSource = new CancellationTokenSource();
const { token } = cancellationSource; const { token } = cancellationSource;
const progress = await messageService.showProgress( const progress = await messageService.showProgress(
{ text, options: { cancelable: false } }, { text, options: { cancelable } },
() => cancellationSource.cancel() () => cancellationSource.cancel()
); );
try { try {

View File

@ -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 { FileUri } from '@theia/core/lib/node/file-uri';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { relative } from 'node:path';
import * as jspb from 'google-protobuf'; import * as jspb from 'google-protobuf';
import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb'; 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 { import {
CompilerWarnings,
CoreService,
CoreError,
CompileSummary, CompileSummary,
CompilerWarnings,
CoreError,
CoreService,
isCompileSummary, isCompileSummary,
isUploadResponse, isUploadResponse,
} from '../common/protocol/core-service'; } 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 { import {
CompileRequest, CompileRequest,
CompileResponse, CompileResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb'; } 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 { import {
BurnBootloaderRequest, BurnBootloaderRequest,
BurnBootloaderResponse, BurnBootloaderResponse,
@ -25,26 +47,13 @@ import {
UploadUsingProgrammerRequest, UploadUsingProgrammerRequest,
UploadUsingProgrammerResponse, UploadUsingProgrammerResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
import { ResponseService } from '../common/protocol/response-service'; import { CoreClientAware } from './core-client-provider';
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 { 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 { ExecuteWithProgress, ProgressResponse } from './grpc-progressible';
import type { Mutable } from '@theia/core/lib/common/types'; import { MonitorManager } from './monitor-manager';
import { BoardDiscovery, createApiPort } from './board-discovery'; import { ServiceError } from './service-error';
import { AutoFlushingBuffer } from './utils/buffers';
import { userAbort } from '../common/nls';
import { UserAbortApplicationError } from '../common/protocol/progressible';
namespace Uploadable { namespace Uploadable {
export type Request = UploadRequest | UploadUsingProgrammerRequest; export type Request = UploadRequest | UploadUsingProgrammerRequest;
@ -64,9 +73,13 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
@inject(BoardDiscovery) @inject(BoardDiscovery)
private readonly boardDiscovery: BoardDiscovery; private readonly boardDiscovery: BoardDiscovery;
async compile(options: CoreService.Options.Compile): Promise<void> { async compile(
options: CoreService.Options.Compile,
cancellationToken?: CancellationToken
): Promise<void> {
const coreClient = await this.coreClient; const coreClient = await this.coreClient;
const { client, instance } = coreClient; const { client, instance } = coreClient;
const request = this.compileRequest(options, instance);
const compileSummary = <CompileSummaryFragment>{}; const compileSummary = <CompileSummaryFragment>{};
const progressHandler = this.createProgressHandler(options); const progressHandler = this.createProgressHandler(options);
const compileSummaryHandler = (response: CompileResponse) => const compileSummaryHandler = (response: CompileResponse) =>
@ -75,10 +88,15 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
progressHandler, progressHandler,
compileSummaryHandler compileSummaryHandler
); );
const request = this.compileRequest(options, instance); const toDisposeOnFinally = new DisposableCollection(handler);
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
client const call = client.compile(request);
.compile(request) if (cancellationToken) {
toDisposeOnFinally.push(
cancellationToken.onCancellationRequested(() => call.cancel())
);
}
call
.on('data', handler.onData) .on('data', handler.onData)
.on('error', (error) => { .on('error', (error) => {
if (!ServiceError.is(error)) { if (!ServiceError.is(error)) {
@ -87,7 +105,13 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
error error
); );
reject(error); reject(error);
} else { return;
}
if (ServiceError.isCancel(error)) {
console.log(userAbort);
reject(UserAbortApplicationError());
return;
}
const compilerErrors = tryParseError({ const compilerErrors = tryParseError({
content: handler.content, content: handler.content,
sketch: options.sketch, sketch: options.sketch,
@ -105,12 +129,15 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
OutputMessage.Severity.Error OutputMessage.Severity.Error
); );
reject(CoreError.VerifyFailed(message, compilerErrors)); reject(CoreError.VerifyFailed(message, compilerErrors));
}
}) })
.on('end', resolve); .on('end', resolve);
}).finally(() => { }).finally(() => {
handler.dispose(); toDisposeOnFinally.dispose();
if (!isCompileSummary(compileSummary)) { if (!isCompileSummary(compileSummary)) {
if (cancellationToken && cancellationToken.isCancellationRequested) {
// NOOP
return;
}
console.error( console.error(
`Have not received the full compile summary from the CLI while running the compilation. ${JSON.stringify( `Have not received the full compile summary from the CLI while running the compilation. ${JSON.stringify(
compileSummary compileSummary
@ -176,7 +203,10 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
return request; return request;
} }
upload(options: CoreService.Options.Upload): Promise<ApiUploadResponse> { upload(
options: CoreService.Options.Upload,
cancellationToken?: CancellationToken
): Promise<ApiUploadResponse> {
const { usingProgrammer } = options; const { usingProgrammer } = options;
return this.doUpload( return this.doUpload(
options, options,
@ -190,7 +220,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
usingProgrammer usingProgrammer
? CoreError.UploadUsingProgrammerFailed ? CoreError.UploadUsingProgrammerFailed
: CoreError.UploadFailed, : CoreError.UploadFailed,
`upload${usingProgrammer ? ' using programmer' : ''}` `upload${usingProgrammer ? ' using programmer' : ''}`,
cancellationToken
); );
} }
@ -204,7 +235,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
client: ArduinoCoreServiceClient client: ArduinoCoreServiceClient
) => (request: REQ) => ClientReadableStream<RESP>, ) => (request: REQ) => ClientReadableStream<RESP>,
errorCtor: ApplicationError.Constructor<number, CoreError.ErrorLocation[]>, errorCtor: ApplicationError.Constructor<number, CoreError.ErrorLocation[]>,
task: string task: string,
cancellationToken?: CancellationToken
): Promise<ApiUploadResponse> { ): Promise<ApiUploadResponse> {
const portBeforeUpload = options.port; const portBeforeUpload = options.port;
const uploadResponseFragment: Mutable<Partial<ApiUploadResponse>> = { const uploadResponseFragment: Mutable<Partial<ApiUploadResponse>> = {
@ -241,16 +273,31 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
progressHandler, progressHandler,
updateUploadResponseFragmentHandler updateUploadResponseFragmentHandler
); );
const toDisposeOnFinally = new DisposableCollection(handler);
const grpcCall = responseFactory(client); const grpcCall = responseFactory(client);
return this.notifyUploadWillStart(options).then(() => return this.notifyUploadWillStart(options).then(() =>
new Promise<ApiUploadResponse>((resolve, reject) => { new Promise<ApiUploadResponse>((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('data', handler.onData)
.on('error', (error) => { .on('error', (error) => {
if (!ServiceError.is(error)) { if (!ServiceError.is(error)) {
console.error(`Unexpected error occurred while ${task}.`, error); console.error(`Unexpected error occurred while ${task}.`, error);
reject(error); reject(error);
} else { return;
}
if (ServiceError.isCancel(error)) {
console.log(userAbort);
reject(UserAbortApplicationError());
return;
}
const message = nls.localize( const message = nls.localize(
'arduino/upload/error', 'arduino/upload/error',
'{0} error: {1}', '{0} error: {1}',
@ -267,7 +314,6 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
}) })
) )
); );
}
}) })
.on('end', () => { .on('end', () => {
if (isUploadResponse(uploadResponseFragment)) { if (isUploadResponse(uploadResponseFragment)) {
@ -285,7 +331,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
} }
}); });
}).finally(async () => { }).finally(async () => {
handler.dispose(); toDisposeOnFinally.dispose();
await this.notifyUploadDidFinish( await this.notifyUploadDidFinish(
Object.assign(options, { Object.assign(options, {
afterPort: uploadResponseFragment.portAfterUpload, afterPort: uploadResponseFragment.portAfterUpload,
@ -320,16 +366,25 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
return request; return request;
} }
async burnBootloader(options: CoreService.Options.Bootloader): Promise<void> { async burnBootloader(
options: CoreService.Options.Bootloader,
cancellationToken?: CancellationToken
): Promise<void> {
const coreClient = await this.coreClient; const coreClient = await this.coreClient;
const { client, instance } = coreClient; const { client, instance } = coreClient;
const progressHandler = this.createProgressHandler(options); const progressHandler = this.createProgressHandler(options);
const handler = this.createOnDataHandler(progressHandler); const handler = this.createOnDataHandler(progressHandler);
const request = this.burnBootloaderRequest(options, instance); const request = this.burnBootloaderRequest(options, instance);
const toDisposeOnFinally = new DisposableCollection(handler);
return this.notifyUploadWillStart(options).then(() => return this.notifyUploadWillStart(options).then(() =>
new Promise<void>((resolve, reject) => { new Promise<void>((resolve, reject) => {
client const call = client.burnBootloader(request);
.burnBootloader(request) if (cancellationToken) {
toDisposeOnFinally.push(
cancellationToken.onCancellationRequested(() => call.cancel())
);
}
call
.on('data', handler.onData) .on('data', handler.onData)
.on('error', (error) => { .on('error', (error) => {
if (!ServiceError.is(error)) { if (!ServiceError.is(error)) {
@ -338,7 +393,13 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
error error
); );
reject(error); reject(error);
} else { return;
}
if (ServiceError.isCancel(error)) {
console.log(userAbort);
reject(UserAbortApplicationError());
return;
}
this.sendResponse(error.details, OutputMessage.Severity.Error); this.sendResponse(error.details, OutputMessage.Severity.Error);
reject( reject(
CoreError.BurnBootloaderFailed( CoreError.BurnBootloaderFailed(
@ -350,11 +411,10 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
tryParseError({ content: handler.content }) tryParseError({ content: handler.content })
) )
); );
}
}) })
.on('end', resolve); .on('end', resolve);
}).finally(async () => { }).finally(async () => {
handler.dispose(); toDisposeOnFinally.dispose();
await this.notifyUploadDidFinish( await this.notifyUploadDidFinish(
Object.assign(options, { afterPort: options.port }) 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)) { for (const uri of Object.keys(options.sourceOverride)) {
const content = options.sourceOverride[uri]; const content = options.sourceOverride[uri];
if (content) { if (content) {
const relativePath = relative(sketchPath, FileUri.fsPath(uri)); const relativePath = path.relative(sketchPath, FileUri.fsPath(uri));
req.getSourceOverrideMap().set(relativePath, content); req.getSourceOverrideMap().set(relativePath, content);
} }
} }

View File

@ -152,7 +152,8 @@
"serialMonitor": "Serial Monitor", "serialMonitor": "Serial Monitor",
"type": "Type", "type": "Type",
"unknown": "Unknown", "unknown": "Unknown",
"updateable": "Updatable" "updateable": "Updatable",
"userAbort": "User abort"
}, },
"compile": { "compile": {
"error": "Compilation error: {0}" "error": "Compilation error: {0}"