Show 'progress' indicator during verify/upload.

Closes #575
Closes #1175

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2022-07-27 18:04:40 +02:00 committed by Akos Kitta
parent 27a2a6ca03
commit e156dcc213
13 changed files with 554 additions and 407 deletions

View File

@ -4,11 +4,8 @@ import URI from '@theia/core/lib/common/uri';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import { LibraryService, ResponseServiceClient } from '../../common/protocol';
Installable, import { ExecuteWithProgress } from '../../common/protocol/progressible';
LibraryService,
ResponseServiceClient,
} from '../../common/protocol';
import { import {
SketchContribution, SketchContribution,
Command, Command,
@ -88,7 +85,7 @@ export class AddZipLibrary extends SketchContribution {
private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> { private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
try { try {
await Installable.doWithProgress({ await ExecuteWithProgress.doWithProgress({
messageService: this.messageService, messageService: this.messageService,
progressText: progressText:
nls.localize('arduino/common/processing', 'Processing') + nls.localize('arduino/common/processing', 'Processing') +

View File

@ -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 { ArduinoMenus } from '../menu/arduino-menus';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
CoreServiceContribution,
Command, Command,
CommandRegistry, CommandRegistry,
CoreServiceContribution,
MenuModelRegistry, MenuModelRegistry,
} from './contribution'; } from './contribution';
import { nls } from '@theia/core/lib/common';
@injectable() @injectable()
export class BurnBootloader extends CoreServiceContribution { export class BurnBootloader extends CoreServiceContribution {
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, { registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
execute: () => this.burnBootloader(), execute: () => this.burnBootloader(),
@ -35,32 +28,19 @@ export class BurnBootloader extends CoreServiceContribution {
}); });
} }
async burnBootloader(): Promise<void> { private async burnBootloader(): Promise<void> {
const options = await this.options();
try { try {
const { boardsConfig } = this.boardsServiceClientImpl; await this.doWithProgress({
const port = boardsConfig.selectedPort; progressText: nls.localize(
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = 'arduino/bootloader/burningBootloader',
await Promise.all([ 'Burning bootloader...'
this.boardsDataStore.appendConfigToFqbn( ),
boardsConfig.selectedBoard?.fqbn task: (progressId, coreService) =>
), coreService.burnBootloader({
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn), ...options,
this.preferences.get('arduino.upload.verify'), progressId,
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,
}); });
this.messageService.info( this.messageService.info(
nls.localize( nls.localize(
@ -75,6 +55,27 @@ export class BurnBootloader extends CoreServiceContribution {
this.handleError(e); this.handleError(e);
} }
} }
private async options(): Promise<CoreService.Options.Bootloader> {
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 { export namespace BurnBootloader {

View File

@ -49,13 +49,16 @@ import {
Sketch, Sketch,
CoreService, CoreService,
CoreError, CoreError,
ResponseServiceClient,
} from '../../common/protocol'; } from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences'; import { ArduinoPreferences } from '../arduino-preferences';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { CoreErrorHandler } from './core-error-handler';
import { nls } from '@theia/core'; import { nls } from '@theia/core';
import { OutputChannelManager } from '../theia/output/output-channel'; import { OutputChannelManager } from '../theia/output/output-channel';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; 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 { export {
Command, Command,
@ -167,18 +170,23 @@ export abstract class SketchContribution extends Contribution {
} }
@injectable() @injectable()
export class CoreServiceContribution extends SketchContribution { export abstract class CoreServiceContribution extends SketchContribution {
@inject(CoreService) @inject(BoardsDataStore)
protected readonly coreService: CoreService; protected readonly boardsDataStore: BoardsDataStore;
@inject(CoreErrorHandler) @inject(BoardsServiceProvider)
protected readonly coreErrorHandler: CoreErrorHandler; protected readonly boardsServiceProvider: BoardsServiceProvider;
@inject(CoreService)
private readonly coreService: CoreService;
@inject(ClipboardService) @inject(ClipboardService)
private readonly clipboardService: ClipboardService; private readonly clipboardService: ClipboardService;
@inject(ResponseServiceClient)
private readonly responseService: ResponseServiceClient;
protected handleError(error: unknown): void { protected handleError(error: unknown): void {
this.coreErrorHandler.tryHandle(error);
this.tryToastErrorMessage(error); this.tryToastErrorMessage(error);
} }
@ -214,6 +222,25 @@ export class CoreServiceContribution extends SketchContribution {
throw error; throw error;
} }
} }
protected async doWithProgress<T>(options: {
progressText: string;
keepOutput?: boolean;
task: (progressId: string, coreService: CoreService) => Promise<T>;
}): Promise<T> {
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 { export namespace Contribution {

View File

@ -3,56 +3,47 @@ import { Emitter } from '@theia/core/lib/common/event';
import { BoardUserField, CoreService } from '../../common/protocol'; import { BoardUserField, CoreService } from '../../common/protocol';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
CoreServiceContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
CoreServiceContribution,
} from './contribution'; } from './contribution';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog'; import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { DisposableCollection, nls } from '@theia/core/lib/common'; import { DisposableCollection, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import type { VerifySketchParams } from './verify-sketch';
@injectable() @injectable()
export class UploadSketch extends CoreServiceContribution { export class UploadSketch extends CoreServiceContribution {
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; private readonly menuRegistry: MenuModelRegistry;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(UserFieldsDialog) @inject(UserFieldsDialog)
protected readonly userFieldsDialog: UserFieldsDialog; private readonly userFieldsDialog: UserFieldsDialog;
protected cachedUserFields: Map<string, BoardUserField[]> = new Map(); private boardRequiresUserFields = false;
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
private readonly menuActionsDisposables = new DisposableCollection();
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>(); private readonly onDidChangeEmitter = new Emitter<void>();
readonly onDidChange = this.onDidChangeEmitter.event; private readonly onDidChange = this.onDidChangeEmitter.event;
private uploadInProgress = false;
protected uploadInProgress = false;
protected boardRequiresUserFields = false;
protected readonly menuActionsDisposables = new DisposableCollection();
protected override init(): void { protected override init(): void {
super.init(); super.init();
this.boardsServiceClientImpl.onBoardsConfigChanged(async () => { this.boardsServiceProvider.onBoardsConfigChanged(async () => {
const userFields = const userFields =
await this.boardsServiceClientImpl.selectedBoardUserFields(); await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0; this.boardRequiresUserFields = userFields.length > 0;
this.registerMenus(this.menuRegistry); this.registerMenus(this.menuRegistry);
}); });
} }
private selectedFqbnAddress(): string { private selectedFqbnAddress(): string {
const { boardsConfig } = this.boardsServiceClientImpl; const { boardsConfig } = this.boardsServiceProvider;
const fqbn = boardsConfig.selectedBoard?.fqbn; const fqbn = boardsConfig.selectedBoard?.fqbn;
if (!fqbn) { if (!fqbn) {
return ''; return '';
@ -76,7 +67,7 @@ export class UploadSketch extends CoreServiceContribution {
if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) { if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) {
// Deep clone the array of board fields to avoid editing the cached ones // Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = ( this.userFieldsDialog.value = (
await this.boardsServiceClientImpl.selectedBoardUserFields() await this.boardsServiceProvider.selectedBoardUserFields()
).map((f) => ({ ...f })); ).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open(); const result = await this.userFieldsDialog.open();
if (!result) { if (!result) {
@ -98,8 +89,7 @@ export class UploadSketch extends CoreServiceContribution {
const cached = this.cachedUserFields.get(key); const cached = this.cachedUserFields.get(key);
// Deep clone the array of board fields to avoid editing the cached ones // Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = ( this.userFieldsDialog.value = (
cached ?? cached ?? (await this.boardsServiceProvider.selectedBoardUserFields())
(await this.boardsServiceClientImpl.selectedBoardUserFields())
).map((f) => ({ ...f })); ).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open(); const result = await this.userFieldsDialog.open();
@ -130,7 +120,6 @@ export class UploadSketch extends CoreServiceContribution {
override registerMenus(registry: MenuModelRegistry): void { override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose(); this.menuActionsDisposables.dispose();
this.menuActionsDisposables.push( this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id, commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
@ -153,7 +142,7 @@ export class UploadSketch extends CoreServiceContribution {
new PlaceholderMenuNode( new PlaceholderMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP, ArduinoMenus.SKETCH__MAIN_GROUP,
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id, // commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label!, UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
{ order: '2' } { order: '2' }
) )
) )
@ -193,57 +182,42 @@ export class UploadSketch extends CoreServiceContribution {
} }
async uploadSketch(usingProgrammer = false): Promise<void> { async uploadSketch(usingProgrammer = false): Promise<void> {
// even with buttons disabled, better to double check if an upload is already in progress
if (this.uploadInProgress) { if (this.uploadInProgress) {
return; return;
} }
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
}
try { try {
// toggle the toolbar button and menu item state. // toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not // uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true; this.uploadInProgress = true;
this.coreErrorHandler.reset();
this.onDidChangeEmitter.fire(); 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<boolean>(
'arduino-is-optimize-for-debug'
),
this.preferences.get('arduino.compile.verbose'),
]);
const verbose = { compile: compileVerbose, upload: uploadVerbose }; const verifyOptions =
const board = { await this.commandService.executeCommand<CoreService.Options.Compile>(
...boardsConfig.selectedBoard, 'arduino-verify-sketch',
name: boardsConfig.selectedBoard?.name || '', <VerifySketchParams>{
fqbn, exportBinaries: false,
}; silent: true,
let options: CoreService.Upload.Options | undefined = undefined; }
const { selectedPort } = boardsConfig; );
const port = selectedPort; if (!verifyOptions) {
const userFields = return;
this.cachedUserFields.get(this.selectedFqbnAddress()) ?? []; }
if (userFields.length === 0 && this.boardRequiresUserFields) {
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( this.messageService.error(
nls.localize( nls.localize(
'arduino/sketch/userFieldsNotFoundError', 'arduino/sketch/userFieldsNotFoundError',
@ -253,37 +227,13 @@ export class UploadSketch extends CoreServiceContribution {
return; return;
} }
if (usingProgrammer) { await this.doWithProgress({
const programmer = selectedProgrammer; progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'),
options = { task: (progressId, coreService) =>
sketch, coreService.upload({ ...uploadOptions, progressId }),
board, keepOutput: true,
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);
}
this.messageService.info( this.messageService.info(
nls.localize('arduino/sketch/doneUploading', 'Done uploading.'), nls.localize('arduino/sketch/doneUploading', 'Done uploading.'),
{ timeout: 3000 } { timeout: 3000 }
@ -295,6 +245,52 @@ export class UploadSketch extends CoreServiceContribution {
this.onDidChangeEmitter.fire(); this.onDidChangeEmitter.fire();
} }
} }
private async uploadOptions(
usingProgrammer: boolean,
verifyOptions: CoreService.Options.Compile
): Promise<CoreService.Options.Upload | undefined> {
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 { export namespace UploadSketch {
@ -302,7 +298,7 @@ export namespace UploadSketch {
export const UPLOAD_SKETCH: Command = { export const UPLOAD_SKETCH: Command = {
id: 'arduino-upload-sketch', id: 'arduino-upload-sketch',
}; };
export const UPLOAD_WITH_CONFIGURATION: Command = { export const UPLOAD_WITH_CONFIGURATION: Command & { label: string } = {
id: 'arduino-upload-with-configuration-sketch', id: 'arduino-upload-with-configuration-sketch',
label: nls.localize( label: nls.localize(
'arduino/sketch/configureAndUpload', 'arduino/sketch/configureAndUpload',

View File

@ -2,8 +2,6 @@ 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 { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
CoreServiceContribution, CoreServiceContribution,
Command, Command,
@ -14,27 +12,36 @@ import {
} from './contribution'; } from './contribution';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; 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() @injectable()
export class VerifySketch extends CoreServiceContribution { export class VerifySketch extends CoreServiceContribution {
@inject(BoardsDataStore) @inject(CoreErrorHandler)
protected readonly boardsDataStore: BoardsDataStore; private readonly coreErrorHandler: CoreErrorHandler;
@inject(BoardsServiceProvider) private readonly onDidChangeEmitter = new Emitter<void>();
protected readonly boardsServiceClientImpl: BoardsServiceProvider; private readonly onDidChange = this.onDidChangeEmitter.event;
private verifyInProgress = false;
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected verifyInProgress = false;
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
execute: () => this.verifySketch(), execute: (params?: VerifySketchParams) => this.verifySketch(params),
isEnabled: () => !this.verifyInProgress, isEnabled: () => !this.verifyInProgress,
}); });
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, { registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
execute: () => this.verifySketch(true), execute: () => this.verifySketch({ exportBinaries: true }),
isEnabled: () => !this.verifyInProgress, isEnabled: () => !this.verifyInProgress,
}); });
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
@ -84,61 +91,87 @@ export class VerifySketch extends CoreServiceContribution {
}); });
} }
async verifySketch(exportBinaries?: boolean): Promise<void> { protected override handleError(error: unknown): void {
// even with buttons disabled, better to double check if a verify is already in progress this.coreErrorHandler.tryHandle(error);
super.handleError(error);
}
private async verifySketch(
params?: VerifySketchParams
): Promise<CoreService.Options.Compile | undefined> {
if (this.verifyInProgress) { 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 { try {
this.verifyInProgress = true; if (!params?.silent) {
this.verifyInProgress = true;
this.onDidChangeEmitter.fire();
}
this.coreErrorHandler.reset(); this.coreErrorHandler.reset();
this.onDidChangeEmitter.fire();
const { boardsConfig } = this.boardsServiceClientImpl; const options = await this.options(params?.exportBinaries);
const [fqbn, sourceOverride] = await Promise.all([ if (!options) {
this.boardsDataStore.appendConfigToFqbn( return undefined;
boardsConfig.selectedBoard?.fqbn }
await this.doWithProgress({
progressText: nls.localize(
'arduino/sketch/compile',
'Compiling sketch...'
), ),
this.sourceOverride(), task: (progressId, coreService) =>
]); coreService.compile({
const board = { ...options,
...boardsConfig.selectedBoard, progressId,
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<boolean>(
'arduino-is-optimize-for-debug'
);
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.compile({
sketch,
board,
optimizeForDebug: Boolean(optimizeForDebug),
verbose,
exportBinaries,
sourceOverride,
compilerWarnings,
}); });
this.messageService.info( this.messageService.info(
nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'), nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'),
{ timeout: 3000 } { 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) { } catch (e) {
this.handleError(e); this.handleError(e);
return undefined;
} finally { } finally {
this.verifyInProgress = false; this.verifyInProgress = false;
this.onDidChangeEmitter.fire(); if (!params?.silent) {
this.onDidChangeEmitter.fire();
}
} }
} }
private async options(
exportBinaries?: boolean
): Promise<CoreService.Options.Compile | undefined> {
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<boolean>(
'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 { export namespace VerifySketch {

View File

@ -5,6 +5,7 @@ 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 { Installable } from '../../../common/protocol/installable'; import { 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';
@ -111,7 +112,7 @@ export class FilterableListContainer<
version: Installable.Version version: Installable.Version
): Promise<void> { ): Promise<void> {
const { install, searchable } = this.props; const { install, searchable } = this.props;
await Installable.doWithProgress({ await ExecuteWithProgress.doWithProgress({
...this.props, ...this.props,
progressText: progressText:
nls.localize('arduino/common/processing', 'Processing') + nls.localize('arduino/common/processing', 'Processing') +
@ -137,7 +138,7 @@ export class FilterableListContainer<
return; return;
} }
const { uninstall, searchable } = this.props; const { uninstall, searchable } = this.props;
await Installable.doWithProgress({ await ExecuteWithProgress.doWithProgress({
...this.props, ...this.props,
progressText: progressText:
nls.localize('arduino/common/processing', 'Processing') + nls.localize('arduino/common/processing', 'Processing') +

View File

@ -1,7 +1,6 @@
import { ApplicationError } from '@theia/core/lib/common/application-error'; import { ApplicationError } from '@theia/core/lib/common/application-error';
import type { Location } from '@theia/core/shared/vscode-languageserver-protocol'; import type { Location } from '@theia/core/shared/vscode-languageserver-protocol';
import type { import type {
Board,
BoardUserField, BoardUserField,
Port, Port,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
@ -60,46 +59,39 @@ export namespace CoreError {
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( compile(options: CoreService.Options.Compile): Promise<void>;
options: CoreService.Compile.Options & upload(options: CoreService.Options.Upload): Promise<void>;
Readonly<{ burnBootloader(options: CoreService.Options.Bootloader): Promise<void>;
exportBinaries?: boolean;
compilerWarnings?: CompilerWarnings;
}>
): Promise<void>;
upload(options: CoreService.Upload.Options): Promise<void>;
uploadUsingProgrammer(options: CoreService.Upload.Options): Promise<void>;
burnBootloader(options: CoreService.Bootloader.Options): Promise<void>;
} }
export namespace CoreService { export namespace CoreService {
export namespace Compile { export namespace Options {
export interface 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 sketch: Sketch;
readonly board?: Board;
readonly optimizeForDebug: boolean;
readonly verbose: boolean;
readonly sourceOverride: Record<string, string>;
} }
} export interface BoardBased {
export namespace Upload {
export interface Options extends Omit<Compile.Options, 'verbose'> {
readonly port?: Port; readonly port?: Port;
readonly programmer?: Programmer | undefined; readonly programmer?: Programmer | undefined;
readonly verify: boolean; /**
* For the _Verify after upload_ setting.
*/
readonly verify: boolean; // TODO: (API) why not optional with false as the default value?
}
export interface Compile extends Base, SketchBased {
readonly optimizeForDebug: boolean; // TODO: (API) make this optional
readonly sourceOverride: Record<string, string>; // TODO: (API) make this optional
readonly exportBinaries?: boolean;
readonly compilerWarnings?: CompilerWarnings;
}
export interface Upload extends Base, SketchBased, BoardBased {
readonly userFields: BoardUserField[]; readonly userFields: BoardUserField[];
readonly verbose: { compile: boolean; upload: boolean }; readonly usingProgrammer?: boolean;
}
}
export namespace Bootloader {
export interface Options {
readonly board?: Board;
readonly port?: Port;
readonly programmer?: Programmer | undefined;
readonly verbose: boolean;
readonly verify: boolean;
} }
export interface Bootloader extends Base, BoardBased {}
} }
} }

View File

@ -1,10 +1,6 @@
import * as semver from 'semver'; import * as semver from 'semver';
import type { Progress } from '@theia/core/lib/common/message-service-protocol'; import { ExecuteWithProgress } from './progressible';
import { import { naturalCompare } from '../utils';
CancellationToken,
CancellationTokenSource,
} from '@theia/core/lib/common/cancellation';
import { naturalCompare } from './../utils';
import type { ArduinoComponent } from './arduino-component'; import type { ArduinoComponent } from './arduino-component';
import type { MessageService } from '@theia/core/lib/common/message-service'; import type { MessageService } from '@theia/core/lib/common/message-service';
import type { ResponseServiceClient } from './response-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.) * 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)) { if (semver.valid(left) && semver.valid(right)) {
return semver.compare(left, right); return semver.compare(left, right);
} }
@ -50,7 +46,7 @@ export namespace Installable {
version: Installable.Version; version: Installable.Version;
}): Promise<void> { }): Promise<void> {
const { item, version } = options; const { item, version } = options;
return doWithProgress({ return ExecuteWithProgress.doWithProgress({
...options, ...options,
progressText: `Processing ${item.name}:${version}`, progressText: `Processing ${item.name}:${version}`,
run: ({ progressId }) => run: ({ progressId }) =>
@ -71,7 +67,7 @@ export namespace Installable {
item: T; item: T;
}): Promise<void> { }): Promise<void> {
const { item } = options; const { item } = options;
return doWithProgress({ return ExecuteWithProgress.doWithProgress({
...options, ...options,
progressText: `Processing ${item.name}${ progressText: `Processing ${item.name}${
item.installedVersion ? `:${item.installedVersion}` : '' item.installedVersion ? `:${item.installedVersion}` : ''
@ -83,51 +79,4 @@ export namespace Installable {
}), }),
}); });
} }
export async function doWithProgress(options: {
run: ({ progressId }: { progressId: string }) => Promise<void>;
messageService: MessageService;
responseService: ResponseServiceClient;
progressText: string;
}): Promise<void> {
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<void>
): Promise<void> {
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();
}
}
} }

View File

@ -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<T>(options: {
run: ({ progressId }: { progressId: string }) => Promise<T>;
messageService: MessageService;
responseService: ResponseServiceClient;
progressText: string;
keepOutput?: boolean;
}): Promise<T> {
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<T>(
text: string,
messageService: MessageService,
cb: (progress: Progress, token: CancellationToken) => Promise<T>
): Promise<T> {
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();
}
}
}

View File

@ -46,5 +46,5 @@ export interface ResponseService {
export const ResponseServiceClient = Symbol('ResponseServiceClient'); export const ResponseServiceClient = Symbol('ResponseServiceClient');
export interface ResponseServiceClient extends ResponseService { export interface ResponseServiceClient extends ResponseService {
onProgressDidChange: Event<ProgressMessage>; onProgressDidChange: Event<ProgressMessage>;
clearOutput: () => void; clearOutput: () => void; // TODO: this should not belong here.
} }

View File

@ -33,6 +33,12 @@ import { tryParseError } from './cli-error-parser';
import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
import { firstToUpperCase, notEmpty } from '../common/utils'; import { firstToUpperCase, notEmpty } from '../common/utils';
import { ServiceError } from './service-error'; import { ServiceError } from './service-error';
import { ExecuteWithProgress, ProgressResponse } from './grpc-progressible';
namespace Uploadable {
export type Request = UploadRequest | UploadUsingProgrammerRequest;
export type Response = UploadResponse | UploadUsingProgrammerResponse;
}
@injectable() @injectable()
export class CoreServiceImpl extends CoreClientAware implements CoreService { export class CoreServiceImpl extends CoreClientAware implements CoreService {
@ -45,27 +51,27 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
@inject(CommandService) @inject(CommandService)
private readonly commandService: CommandService; private readonly commandService: CommandService;
async compile( async compile(options: CoreService.Options.Compile): Promise<void> {
options: CoreService.Compile.Options & {
exportBinaries?: boolean;
compilerWarnings?: CompilerWarnings;
}
): Promise<void> {
const coreClient = await this.coreClient; const coreClient = await this.coreClient;
const { client, instance } = coreClient; const { client, instance } = coreClient;
let buildPath: string | undefined = undefined; let buildPath: string | undefined = undefined;
const handler = this.createOnDataHandler<CompileResponse>((response) => { const progressHandler = this.createProgressHandler(options);
const buildPathHandler = (response: CompileResponse) => {
const currentBuildPath = response.getBuildPath(); const currentBuildPath = response.getBuildPath();
if (!buildPath && currentBuildPath) { if (currentBuildPath) {
buildPath = currentBuildPath; buildPath = currentBuildPath;
} else { } else {
if (!!currentBuildPath && currentBuildPath !== buildPath) { if (!!buildPath && currentBuildPath !== buildPath) {
throw new Error( 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<CompileResponse>(
progressHandler,
buildPathHandler
);
const request = this.compileRequest(options, instance); const request = this.compileRequest(options, instance);
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
client client
@ -132,20 +138,20 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
} }
private compileRequest( private compileRequest(
options: CoreService.Compile.Options & { options: CoreService.Options.Compile & {
exportBinaries?: boolean; exportBinaries?: boolean;
compilerWarnings?: CompilerWarnings; compilerWarnings?: CompilerWarnings;
}, },
instance: Instance instance: Instance
): CompileRequest { ): CompileRequest {
const { sketch, board, compilerWarnings } = options; const { sketch, fqbn, compilerWarnings } = options;
const sketchUri = sketch.uri; const sketchUri = sketch.uri;
const sketchPath = FileUri.fsPath(sketchUri); const sketchPath = FileUri.fsPath(sketchUri);
const request = new CompileRequest(); const request = new CompileRequest();
request.setInstance(instance); request.setInstance(instance);
request.setSketchPath(sketchPath); request.setSketchPath(sketchPath);
if (board?.fqbn) { if (fqbn) {
request.setFqbn(board.fqbn); request.setFqbn(fqbn);
} }
if (compilerWarnings) { if (compilerWarnings) {
request.setWarnings(compilerWarnings.toLowerCase()); request.setWarnings(compilerWarnings.toLowerCase());
@ -163,60 +169,44 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
return request; return request;
} }
upload(options: CoreService.Upload.Options): Promise<void> { upload(options: CoreService.Options.Upload): Promise<void> {
const { usingProgrammer } = options;
return this.doUpload( return this.doUpload(
options, options,
() => new UploadRequest(), usingProgrammer
(client, req) => client.upload(req), ? new UploadUsingProgrammerRequest()
(message: string, locations: CoreError.ErrorLocation[]) => : new UploadRequest(),
CoreError.UploadFailed(message, locations), (client) =>
'upload' (usingProgrammer ? client.uploadUsingProgrammer : client.upload).bind(
client
),
usingProgrammer
? CoreError.UploadUsingProgrammerFailed
: CoreError.UploadFailed,
`upload${usingProgrammer ? ' using programmer' : ''}`
); );
} }
async uploadUsingProgrammer( protected async doUpload<
options: CoreService.Upload.Options REQ extends Uploadable.Request,
): Promise<void> { RESP extends Uploadable.Response
return this.doUpload( >(
options, options: CoreService.Options.Upload,
() => new UploadUsingProgrammerRequest(), request: REQ,
(client, req) => client.uploadUsingProgrammer(req), responseFactory: (
(message: string, locations: CoreError.ErrorLocation[]) => client: ArduinoCoreServiceClient
CoreError.UploadUsingProgrammerFailed(message, locations), ) => (request: REQ) => ClientReadableStream<RESP>,
'upload using programmer' errorCtor: ApplicationError.Constructor<number, CoreError.ErrorLocation[]>,
);
}
protected async doUpload(
options: CoreService.Upload.Options,
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest,
responseHandler: (
client: ArduinoCoreServiceClient,
request: UploadRequest | UploadUsingProgrammerRequest
) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
errorHandler: (
message: string,
locations: CoreError.ErrorLocation[]
) => ApplicationError<number, CoreError.ErrorLocation[]>,
task: string task: string
): Promise<void> { ): Promise<void> {
await this.compile({
...options,
verbose: options.verbose.compile,
exportBinaries: false,
});
const coreClient = await this.coreClient; const coreClient = await this.coreClient;
const { client, instance } = coreClient; const { client, instance } = coreClient;
const request = this.uploadOrUploadUsingProgrammerRequest( const progressHandler = this.createProgressHandler(options);
options, const handler = this.createOnDataHandler(progressHandler);
instance, const grpcCall = responseFactory(client);
requestFactory
);
const handler = this.createOnDataHandler();
return this.notifyUploadWillStart(options).then(() => return this.notifyUploadWillStart(options).then(() =>
new Promise<void>((resolve, reject) => { new Promise<void>((resolve, reject) => {
responseHandler(client, request) grpcCall(this.initUploadRequest(request, options, instance))
.on('data', handler.onData) .on('data', handler.onData)
.on('error', (error) => { .on('error', (error) => {
if (!ServiceError.is(error)) { if (!ServiceError.is(error)) {
@ -231,7 +221,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
); );
this.sendResponse(error.details, OutputMessage.Severity.Error); this.sendResponse(error.details, OutputMessage.Severity.Error);
reject( reject(
errorHandler( errorCtor(
message, message,
tryParseError({ tryParseError({
content: handler.stderr, content: handler.stderr,
@ -249,24 +239,23 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
); );
} }
private uploadOrUploadUsingProgrammerRequest( private initUploadRequest<REQ extends Uploadable.Request>(
options: CoreService.Upload.Options, request: REQ,
instance: Instance, options: CoreService.Options.Upload,
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest instance: Instance
): UploadRequest | UploadUsingProgrammerRequest { ): REQ {
const { sketch, board, port, programmer } = options; const { sketch, fqbn, port, programmer } = options;
const sketchPath = FileUri.fsPath(sketch.uri); const sketchPath = FileUri.fsPath(sketch.uri);
const request = requestFactory();
request.setInstance(instance); request.setInstance(instance);
request.setSketchPath(sketchPath); request.setSketchPath(sketchPath);
if (board?.fqbn) { if (fqbn) {
request.setFqbn(board.fqbn); request.setFqbn(fqbn);
} }
request.setPort(this.createPort(port)); request.setPort(this.createPort(port));
if (programmer) { if (programmer) {
request.setProgrammer(programmer.id); request.setProgrammer(programmer.id);
} }
request.setVerbose(options.verbose.upload); request.setVerbose(options.verbose);
request.setVerify(options.verify); request.setVerify(options.verify);
options.userFields.forEach((e) => { options.userFields.forEach((e) => {
@ -275,10 +264,11 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
return request; return request;
} }
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> { async burnBootloader(options: CoreService.Options.Bootloader): Promise<void> {
const coreClient = await this.coreClient; const coreClient = await this.coreClient;
const { client, instance } = 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); const request = this.burnBootloaderRequest(options, instance);
return this.notifyUploadWillStart(options).then(() => return this.notifyUploadWillStart(options).then(() =>
new Promise<void>((resolve, reject) => { new Promise<void>((resolve, reject) => {
@ -315,14 +305,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
} }
private burnBootloaderRequest( private burnBootloaderRequest(
options: CoreService.Bootloader.Options, options: CoreService.Options.Bootloader,
instance: Instance instance: Instance
): BurnBootloaderRequest { ): BurnBootloaderRequest {
const { board, port, programmer } = options; const { fqbn, port, programmer } = options;
const request = new BurnBootloaderRequest(); const request = new BurnBootloaderRequest();
request.setInstance(instance); request.setInstance(instance);
if (board?.fqbn) { if (fqbn) {
request.setFqbn(board.fqbn); request.setFqbn(fqbn);
} }
request.setPort(this.createPort(port)); request.setPort(this.createPort(port));
if (programmer) { if (programmer) {
@ -333,8 +323,24 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
return request; return request;
} }
private createProgressHandler<R extends ProgressResponse>(
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<R>({
progressId: options.progressId,
responseService: this.responseService,
});
}
private createOnDataHandler<R extends StreamingResponse>( private createOnDataHandler<R extends StreamingResponse>(
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 & { ): Disposable & {
stderr: Buffer[]; stderr: Buffer[];
onData: (response: R) => void; onData: (response: R) => void;
@ -347,14 +353,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
} }
}); });
}); });
const onData = StreamingResponse.createOnDataHandler( const onData = StreamingResponse.createOnDataHandler({
stderr, stderr,
(out, err) => { onData: (out, err) => {
buffer.addChunk(out); buffer.addChunk(out);
buffer.addChunk(err, OutputMessage.Severity.Error); buffer.addChunk(err, OutputMessage.Severity.Error);
}, },
onResponse handlers,
); });
return { return {
dispose: () => buffer.dispose(), dispose: () => buffer.dispose(),
stderr, stderr,
@ -391,7 +397,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
private mergeSourceOverrides( private mergeSourceOverrides(
req: { getSourceOverrideMap(): jspb.Map<string, string> }, req: { getSourceOverrideMap(): jspb.Map<string, string> },
options: CoreService.Compile.Options options: CoreService.Options.Compile
): void { ): void {
const sketchPath = FileUri.fsPath(options.sketch.uri); const sketchPath = FileUri.fsPath(options.sketch.uri);
for (const uri of Object.keys(options.sourceOverride)) { for (const uri of Object.keys(options.sourceOverride)) {
@ -422,18 +428,24 @@ type StreamingResponse =
namespace StreamingResponse { namespace StreamingResponse {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createOnDataHandler<R extends StreamingResponse>( export function createOnDataHandler<R extends StreamingResponse>(
stderr: Uint8Array[], options: StreamingResponse.Options<R>
onData: (out: Uint8Array, err: Uint8Array) => void,
onResponse?: (response: R) => void
): (response: R) => void { ): (response: R) => void {
return (response: R) => { return (response: R) => {
const out = response.getOutStream_asU8(); const out = response.getOutStream_asU8();
const err = response.getErrStream_asU8(); const err = response.getErrStream_asU8();
stderr.push(err); options.stderr.push(err);
onData(out, err); options.onData(out, err);
if (onResponse) { options.handlers?.forEach((handler) => handler(response));
onResponse(response);
}
}; };
} }
export interface Options<R extends StreamingResponse> {
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)[];
}
} }

View File

@ -12,6 +12,7 @@ import {
DownloadProgress, DownloadProgress,
TaskProgress, TaskProgress,
} from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
import { CompileResponse } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb';
import { import {
PlatformInstallResponse, PlatformInstallResponse,
PlatformUninstallResponse, PlatformUninstallResponse,
@ -21,6 +22,11 @@ import {
LibraryUninstallResponse, LibraryUninstallResponse,
ZipLibraryInstallResponse, ZipLibraryInstallResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/lib_pb'; } 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 = type LibraryProgressResponse =
| LibraryInstallResponse | LibraryInstallResponse
@ -78,15 +84,62 @@ namespace IndexProgressResponse {
return { download: response.getDownloadProgress() }; 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 = export type ProgressResponse =
| LibraryProgressResponse | LibraryProgressResponse
| PlatformProgressResponse | PlatformProgressResponse
| IndexProgressResponse; | IndexProgressResponse
| CoreProgressResponse;
interface UnitOfWork { interface UnitOfWork {
task?: TaskProgress; task?: TaskProgress;
download?: DownloadProgress; 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. * 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}`); console.log(`Progress response [${uuid}]: ${json}`);
} }
} }
const { task, download } = resolve(response); const unitOfWork = resolve(response);
const { task, download } = unitOfWork;
if (!download && !task) { if (!download && !task) {
console.warn( // report a fake unknown progress.
"Implementation error. Neither 'download' nor 'task' is available." if (unitOfWork === UnitOfWork.Unknown && progressId) {
); if (progressId) {
// This is still an API error from the CLI, but IDE2 ignores it. responseService.reportProgress?.({
// Technically, it does not cause an error, but could mess up the progress reporting. progressId,
// See an example of an empty object `{}` repose here: https://github.com/arduino/arduino-ide/issues/906#issuecomment-1171145630. 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; return;
} }
if (task && download) { if (task && download) {
@ -132,6 +199,7 @@ export namespace ExecuteWithProgress {
} }
if (task) { if (task) {
const message = task.getName() || task.getMessage(); const message = task.getName() || task.getMessage();
const percent = task.getPercent();
if (message) { if (message) {
if (progressId) { if (progressId) {
responseService.reportProgress?.({ responseService.reportProgress?.({
@ -141,6 +209,14 @@ export namespace ExecuteWithProgress {
}); });
} }
responseService.appendToOutput?.({ chunk: `${message}\n` }); responseService.appendToOutput?.({ chunk: `${message}\n` });
} else if (percent) {
if (progressId) {
responseService.reportProgress?.({
progressId,
message,
work: { done: percent, total: 100 },
});
}
} }
} else if (download) { } else if (download) {
if (download.getFile() && !localFile) { if (download.getFile() && !localFile) {
@ -191,38 +267,38 @@ export namespace ExecuteWithProgress {
return PlatformProgressResponse.workUnit(response); return PlatformProgressResponse.workUnit(response);
} else if (IndexProgressResponse.is(response)) { } else if (IndexProgressResponse.is(response)) {
return IndexProgressResponse.workUnit(response); return IndexProgressResponse.workUnit(response);
} else if (CoreProgressResponse.is(response)) {
return CoreProgressResponse.workUnit(response);
} }
console.warn('Unhandled gRPC response', response); console.warn('Unhandled gRPC response', response);
return {}; return {};
} }
function toJson(response: ProgressResponse): string | undefined { function toJson(response: ProgressResponse): string | undefined {
let object: Record<string, unknown> | undefined = undefined;
if (response instanceof LibraryInstallResponse) { if (response instanceof LibraryInstallResponse) {
return JSON.stringify(LibraryInstallResponse.toObject(false, response)); object = LibraryInstallResponse.toObject(false, response);
} else if (response instanceof LibraryUninstallResponse) { } else if (response instanceof LibraryUninstallResponse) {
return JSON.stringify(LibraryUninstallResponse.toObject(false, response)); object = LibraryUninstallResponse.toObject(false, response);
} else if (response instanceof ZipLibraryInstallResponse) { } else if (response instanceof ZipLibraryInstallResponse) {
return JSON.stringify( object = ZipLibraryInstallResponse.toObject(false, response);
ZipLibraryInstallResponse.toObject(false, response)
);
} else if (response instanceof PlatformInstallResponse) { } else if (response instanceof PlatformInstallResponse) {
return JSON.stringify(PlatformInstallResponse.toObject(false, response)); object = PlatformInstallResponse.toObject(false, response);
} else if (response instanceof PlatformUninstallResponse) { } else if (response instanceof PlatformUninstallResponse) {
return JSON.stringify( object = PlatformUninstallResponse.toObject(false, response);
PlatformUninstallResponse.toObject(false, response)
);
} else if (response instanceof UpdateIndexResponse) { } else if (response instanceof UpdateIndexResponse) {
return JSON.stringify(UpdateIndexResponse.toObject(false, response)); object = UpdateIndexResponse.toObject(false, response);
} else if (response instanceof UpdateLibrariesIndexResponse) { } else if (response instanceof UpdateLibrariesIndexResponse) {
return JSON.stringify( object = UpdateLibrariesIndexResponse.toObject(false, response);
UpdateLibrariesIndexResponse.toObject(false, response)
);
} else if (response instanceof UpdateCoreLibrariesIndexResponse) { } else if (response instanceof UpdateCoreLibrariesIndexResponse) {
return JSON.stringify( object = UpdateCoreLibrariesIndexResponse.toObject(false, response);
UpdateCoreLibrariesIndexResponse.toObject(false, response) } else if (response instanceof CompileResponse) {
); object = CompileResponse.toObject(false, response);
} }
console.warn('Unhandled gRPC response', response); if (!object) {
return undefined; console.warn('Unhandled gRPC response', response);
return undefined;
}
return JSON.stringify(object);
} }
} }

View File

@ -36,6 +36,7 @@
"boardsManager": "Boards Manager", "boardsManager": "Boards Manager",
"bootloader": { "bootloader": {
"burnBootloader": "Burn Bootloader", "burnBootloader": "Burn Bootloader",
"burningBootloader": "Burning bootloader...",
"doneBurningBootloader": "Done burning bootloader." "doneBurningBootloader": "Done burning bootloader."
}, },
"burnBootloader": { "burnBootloader": {
@ -306,6 +307,7 @@
"archiveSketch": "Archive Sketch", "archiveSketch": "Archive Sketch",
"cantOpen": "A folder named \"{0}\" already exists. Can't open sketch.", "cantOpen": "A folder named \"{0}\" already exists. Can't open sketch.",
"close": "Are you sure you want to close the sketch?", "close": "Are you sure you want to close the sketch?",
"compile": "Compiling sketch...",
"configureAndUpload": "Configure And Upload", "configureAndUpload": "Configure And Upload",
"createdArchive": "Created archive '{0}'.", "createdArchive": "Created archive '{0}'.",
"doneCompiling": "Done compiling.", "doneCompiling": "Done compiling.",
@ -327,6 +329,7 @@
"titleSketchbook": "Sketchbook", "titleSketchbook": "Sketchbook",
"upload": "Upload", "upload": "Upload",
"uploadUsingProgrammer": "Upload Using Programmer", "uploadUsingProgrammer": "Upload Using Programmer",
"uploading": "Uploading...",
"userFieldsNotFoundError": "Can't find user fields for connected board", "userFieldsNotFoundError": "Can't find user fields for connected board",
"verify": "Verify", "verify": "Verify",
"verifyOrCompile": "Verify/Compile" "verifyOrCompile": "Verify/Compile"