Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d930d8f

Browse files
author
Akos Kitta
committedAug 1, 2022
Show 'progress' indicator during verify/upload.
Closes #575 Closes #1175 Signed-off-by: Akos Kitta <[email protected]>
1 parent 90d2950 commit d930d8f

File tree

13 files changed

+554
-407
lines changed

13 files changed

+554
-407
lines changed
 

‎arduino-ide-extension/src/browser/contributions/add-zip-library.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@ import URI from '@theia/core/lib/common/uri';
44
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
55
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
66
import { ArduinoMenus } from '../menu/arduino-menus';
7-
import {
8-
Installable,
9-
LibraryService,
10-
ResponseServiceClient,
11-
} from '../../common/protocol';
7+
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
8+
import { ExecuteWithProgress } from '../../common/protocol/progressible';
129
import {
1310
SketchContribution,
1411
Command,
@@ -88,7 +85,7 @@ export class AddZipLibrary extends SketchContribution {
8885

8986
private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
9087
try {
91-
await Installable.doWithProgress({
88+
await ExecuteWithProgress.doWithProgress({
9289
messageService: this.messageService,
9390
progressText:
9491
nls.localize('arduino/common/processing', 'Processing') +

‎arduino-ide-extension/src/browser/contributions/burn-bootloader.ts

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
1-
import { inject, injectable } from '@theia/core/shared/inversify';
1+
import { nls } from '@theia/core/lib/common';
2+
import { injectable } from '@theia/core/shared/inversify';
3+
import { CoreService } from '../../common/protocol';
24
import { ArduinoMenus } from '../menu/arduino-menus';
3-
import { BoardsDataStore } from '../boards/boards-data-store';
4-
import { BoardsServiceProvider } from '../boards/boards-service-provider';
55
import {
6-
CoreServiceContribution,
76
Command,
87
CommandRegistry,
8+
CoreServiceContribution,
99
MenuModelRegistry,
1010
} from './contribution';
11-
import { nls } from '@theia/core/lib/common';
1211

1312
@injectable()
1413
export class BurnBootloader extends CoreServiceContribution {
15-
@inject(BoardsDataStore)
16-
protected readonly boardsDataStore: BoardsDataStore;
17-
18-
@inject(BoardsServiceProvider)
19-
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
20-
2114
override registerCommands(registry: CommandRegistry): void {
2215
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
2316
execute: () => this.burnBootloader(),
@@ -35,32 +28,19 @@ export class BurnBootloader extends CoreServiceContribution {
3528
});
3629
}
3730

38-
async burnBootloader(): Promise<void> {
31+
private async burnBootloader(): Promise<void> {
32+
const options = await this.options();
3933
try {
40-
const { boardsConfig } = this.boardsServiceClientImpl;
41-
const port = boardsConfig.selectedPort;
42-
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
43-
await Promise.all([
44-
this.boardsDataStore.appendConfigToFqbn(
45-
boardsConfig.selectedBoard?.fqbn
46-
),
47-
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
48-
this.preferences.get('arduino.upload.verify'),
49-
this.preferences.get('arduino.upload.verbose'),
50-
]);
51-
52-
const board = {
53-
...boardsConfig.selectedBoard,
54-
name: boardsConfig.selectedBoard?.name || '',
55-
fqbn,
56-
};
57-
this.outputChannelManager.getChannel('Arduino').clear();
58-
await this.coreService.burnBootloader({
59-
board,
60-
programmer,
61-
port,
62-
verify,
63-
verbose,
34+
await this.doWithProgress({
35+
progressText: nls.localize(
36+
'arduino/bootloader/burningBootloader',
37+
'Burning bootloader...'
38+
),
39+
task: (progressId, coreService) =>
40+
coreService.burnBootloader({
41+
...options,
42+
progressId,
43+
}),
6444
});
6545
this.messageService.info(
6646
nls.localize(
@@ -75,6 +55,27 @@ export class BurnBootloader extends CoreServiceContribution {
7555
this.handleError(e);
7656
}
7757
}
58+
59+
private async options(): Promise<CoreService.Options.Bootloader> {
60+
const { boardsConfig } = this.boardsServiceProvider;
61+
const port = boardsConfig.selectedPort;
62+
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
63+
await Promise.all([
64+
this.boardsDataStore.appendConfigToFqbn(
65+
boardsConfig.selectedBoard?.fqbn
66+
),
67+
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
68+
this.preferences.get('arduino.upload.verify'),
69+
this.preferences.get('arduino.upload.verbose'),
70+
]);
71+
return {
72+
fqbn,
73+
programmer,
74+
port,
75+
verify,
76+
verbose,
77+
};
78+
}
7879
}
7980

8081
export namespace BurnBootloader {

‎arduino-ide-extension/src/browser/contributions/contribution.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,16 @@ import {
4949
Sketch,
5050
CoreService,
5151
CoreError,
52+
ResponseServiceClient,
5253
} from '../../common/protocol';
5354
import { ArduinoPreferences } from '../arduino-preferences';
5455
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
55-
import { CoreErrorHandler } from './core-error-handler';
5656
import { nls } from '@theia/core';
5757
import { OutputChannelManager } from '../theia/output/output-channel';
5858
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
59+
import { ExecuteWithProgress } from '../../common/protocol/progressible';
60+
import { BoardsServiceProvider } from '../boards/boards-service-provider';
61+
import { BoardsDataStore } from '../boards/boards-data-store';
5962

6063
export {
6164
Command,
@@ -167,18 +170,23 @@ export abstract class SketchContribution extends Contribution {
167170
}
168171

169172
@injectable()
170-
export class CoreServiceContribution extends SketchContribution {
171-
@inject(CoreService)
172-
protected readonly coreService: CoreService;
173+
export abstract class CoreServiceContribution extends SketchContribution {
174+
@inject(BoardsDataStore)
175+
protected readonly boardsDataStore: BoardsDataStore;
176+
177+
@inject(BoardsServiceProvider)
178+
protected readonly boardsServiceProvider: BoardsServiceProvider;
173179

174-
@inject(CoreErrorHandler)
175-
protected readonly coreErrorHandler: CoreErrorHandler;
180+
@inject(CoreService)
181+
private readonly coreService: CoreService;
176182

177183
@inject(ClipboardService)
178184
private readonly clipboardService: ClipboardService;
179185

186+
@inject(ResponseServiceClient)
187+
private readonly responseService: ResponseServiceClient;
188+
180189
protected handleError(error: unknown): void {
181-
this.coreErrorHandler.tryHandle(error);
182190
this.tryToastErrorMessage(error);
183191
}
184192

@@ -214,6 +222,25 @@ export class CoreServiceContribution extends SketchContribution {
214222
throw error;
215223
}
216224
}
225+
226+
protected async doWithProgress<T>(options: {
227+
progressText: string;
228+
keepOutput?: boolean;
229+
task: (progressId: string, coreService: CoreService) => Promise<T>;
230+
}): Promise<T> {
231+
const { progressText, keepOutput, task } = options;
232+
this.outputChannelManager
233+
.getChannel('Arduino')
234+
.show({ preserveFocus: true });
235+
const result = await ExecuteWithProgress.doWithProgress({
236+
messageService: this.messageService,
237+
responseService: this.responseService,
238+
progressText,
239+
run: ({ progressId }) => task(progressId, this.coreService),
240+
keepOutput,
241+
});
242+
return result;
243+
}
217244
}
218245

219246
export namespace Contribution {

‎arduino-ide-extension/src/browser/contributions/upload-sketch.ts

Lines changed: 96 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,47 @@ import { Emitter } from '@theia/core/lib/common/event';
33
import { BoardUserField, CoreService } from '../../common/protocol';
44
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
55
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
6-
import { BoardsDataStore } from '../boards/boards-data-store';
7-
import { BoardsServiceProvider } from '../boards/boards-service-provider';
86
import {
9-
CoreServiceContribution,
107
Command,
118
CommandRegistry,
129
MenuModelRegistry,
1310
KeybindingRegistry,
1411
TabBarToolbarRegistry,
12+
CoreServiceContribution,
1513
} from './contribution';
1614
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
1715
import { DisposableCollection, nls } from '@theia/core/lib/common';
1816
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
17+
import type { VerifySketchParams } from './verify-sketch';
1918

2019
@injectable()
2120
export class UploadSketch extends CoreServiceContribution {
2221
@inject(MenuModelRegistry)
23-
protected readonly menuRegistry: MenuModelRegistry;
24-
25-
@inject(BoardsDataStore)
26-
protected readonly boardsDataStore: BoardsDataStore;
27-
28-
@inject(BoardsServiceProvider)
29-
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
22+
private readonly menuRegistry: MenuModelRegistry;
3023

3124
@inject(UserFieldsDialog)
32-
protected readonly userFieldsDialog: UserFieldsDialog;
33-
34-
protected cachedUserFields: Map<string, BoardUserField[]> = new Map();
35-
36-
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
37-
readonly onDidChange = this.onDidChangeEmitter.event;
25+
private readonly userFieldsDialog: UserFieldsDialog;
3826

39-
protected uploadInProgress = false;
40-
protected boardRequiresUserFields = false;
27+
private boardRequiresUserFields = false;
28+
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
29+
private readonly menuActionsDisposables = new DisposableCollection();
4130

42-
protected readonly menuActionsDisposables = new DisposableCollection();
31+
private readonly onDidChangeEmitter = new Emitter<void>();
32+
private readonly onDidChange = this.onDidChangeEmitter.event;
33+
private uploadInProgress = false;
4334

4435
protected override init(): void {
4536
super.init();
46-
this.boardsServiceClientImpl.onBoardsConfigChanged(async () => {
37+
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
4738
const userFields =
48-
await this.boardsServiceClientImpl.selectedBoardUserFields();
39+
await this.boardsServiceProvider.selectedBoardUserFields();
4940
this.boardRequiresUserFields = userFields.length > 0;
5041
this.registerMenus(this.menuRegistry);
5142
});
5243
}
5344

5445
private selectedFqbnAddress(): string {
55-
const { boardsConfig } = this.boardsServiceClientImpl;
46+
const { boardsConfig } = this.boardsServiceProvider;
5647
const fqbn = boardsConfig.selectedBoard?.fqbn;
5748
if (!fqbn) {
5849
return '';
@@ -76,7 +67,7 @@ export class UploadSketch extends CoreServiceContribution {
7667
if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) {
7768
// Deep clone the array of board fields to avoid editing the cached ones
7869
this.userFieldsDialog.value = (
79-
await this.boardsServiceClientImpl.selectedBoardUserFields()
70+
await this.boardsServiceProvider.selectedBoardUserFields()
8071
).map((f) => ({ ...f }));
8172
const result = await this.userFieldsDialog.open();
8273
if (!result) {
@@ -98,8 +89,7 @@ export class UploadSketch extends CoreServiceContribution {
9889
const cached = this.cachedUserFields.get(key);
9990
// Deep clone the array of board fields to avoid editing the cached ones
10091
this.userFieldsDialog.value = (
101-
cached ??
102-
(await this.boardsServiceClientImpl.selectedBoardUserFields())
92+
cached ?? (await this.boardsServiceProvider.selectedBoardUserFields())
10393
).map((f) => ({ ...f }));
10494

10595
const result = await this.userFieldsDialog.open();
@@ -130,7 +120,6 @@ export class UploadSketch extends CoreServiceContribution {
130120

131121
override registerMenus(registry: MenuModelRegistry): void {
132122
this.menuActionsDisposables.dispose();
133-
134123
this.menuActionsDisposables.push(
135124
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
136125
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
@@ -153,7 +142,7 @@ export class UploadSketch extends CoreServiceContribution {
153142
new PlaceholderMenuNode(
154143
ArduinoMenus.SKETCH__MAIN_GROUP,
155144
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
156-
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label!,
145+
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
157146
{ order: '2' }
158147
)
159148
)
@@ -193,57 +182,42 @@ export class UploadSketch extends CoreServiceContribution {
193182
}
194183

195184
async uploadSketch(usingProgrammer = false): Promise<void> {
196-
// even with buttons disabled, better to double check if an upload is already in progress
197185
if (this.uploadInProgress) {
198186
return;
199187
}
200188

201-
const sketch = await this.sketchServiceClient.currentSketch();
202-
if (!CurrentSketch.isValid(sketch)) {
203-
return;
204-
}
205-
206189
try {
207190
// toggle the toolbar button and menu item state.
208191
// uploadInProgress will be set to false whether the upload fails or not
209192
this.uploadInProgress = true;
210-
this.coreErrorHandler.reset();
211193
this.onDidChangeEmitter.fire();
212-
const { boardsConfig } = this.boardsServiceClientImpl;
213-
const [
214-
fqbn,
215-
{ selectedProgrammer },
216-
verify,
217-
uploadVerbose,
218-
sourceOverride,
219-
optimizeForDebug,
220-
compileVerbose,
221-
] = await Promise.all([
222-
this.boardsDataStore.appendConfigToFqbn(
223-
boardsConfig.selectedBoard?.fqbn
224-
),
225-
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
226-
this.preferences.get('arduino.upload.verify'),
227-
this.preferences.get('arduino.upload.verbose'),
228-
this.sourceOverride(),
229-
this.commandService.executeCommand<boolean>(
230-
'arduino-is-optimize-for-debug'
231-
),
232-
this.preferences.get('arduino.compile.verbose'),
233-
]);
234194

235-
const verbose = { compile: compileVerbose, upload: uploadVerbose };
236-
const board = {
237-
...boardsConfig.selectedBoard,
238-
name: boardsConfig.selectedBoard?.name || '',
239-
fqbn,
240-
};
241-
let options: CoreService.Upload.Options | undefined = undefined;
242-
const { selectedPort } = boardsConfig;
243-
const port = selectedPort;
244-
const userFields =
245-
this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
246-
if (userFields.length === 0 && this.boardRequiresUserFields) {
195+
const verifyOptions =
196+
await this.commandService.executeCommand<CoreService.Options.Compile>(
197+
'arduino-verify-sketch',
198+
<VerifySketchParams>{
199+
exportBinaries: false,
200+
silent: true,
201+
}
202+
);
203+
if (!verifyOptions) {
204+
return;
205+
}
206+
207+
const uploadOptions = await this.uploadOptions(
208+
usingProgrammer,
209+
verifyOptions
210+
);
211+
if (!uploadOptions) {
212+
return;
213+
}
214+
215+
// TODO: This does not belong here.
216+
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
217+
if (
218+
uploadOptions.userFields.length === 0 &&
219+
this.boardRequiresUserFields
220+
) {
247221
this.messageService.error(
248222
nls.localize(
249223
'arduino/sketch/userFieldsNotFoundError',
@@ -253,37 +227,13 @@ export class UploadSketch extends CoreServiceContribution {
253227
return;
254228
}
255229

256-
if (usingProgrammer) {
257-
const programmer = selectedProgrammer;
258-
options = {
259-
sketch,
260-
board,
261-
optimizeForDebug: Boolean(optimizeForDebug),
262-
programmer,
263-
port,
264-
verbose,
265-
verify,
266-
sourceOverride,
267-
userFields,
268-
};
269-
} else {
270-
options = {
271-
sketch,
272-
board,
273-
optimizeForDebug: Boolean(optimizeForDebug),
274-
port,
275-
verbose,
276-
verify,
277-
sourceOverride,
278-
userFields,
279-
};
280-
}
281-
this.outputChannelManager.getChannel('Arduino').clear();
282-
if (usingProgrammer) {
283-
await this.coreService.uploadUsingProgrammer(options);
284-
} else {
285-
await this.coreService.upload(options);
286-
}
230+
await this.doWithProgress({
231+
progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'),
232+
task: (progressId, coreService) =>
233+
coreService.upload({ ...uploadOptions, progressId }),
234+
keepOutput: true,
235+
});
236+
287237
this.messageService.info(
288238
nls.localize('arduino/sketch/doneUploading', 'Done uploading.'),
289239
{ timeout: 3000 }
@@ -295,14 +245,60 @@ export class UploadSketch extends CoreServiceContribution {
295245
this.onDidChangeEmitter.fire();
296246
}
297247
}
248+
249+
private async uploadOptions(
250+
usingProgrammer: boolean,
251+
verifyOptions: CoreService.Options.Compile
252+
): Promise<CoreService.Options.Upload | undefined> {
253+
const sketch = await this.sketchServiceClient.currentSketch();
254+
if (!CurrentSketch.isValid(sketch)) {
255+
return undefined;
256+
}
257+
const userFields = this.userFields();
258+
const { boardsConfig } = this.boardsServiceProvider;
259+
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
260+
await Promise.all([
261+
verifyOptions.fqbn, // already decorated FQBN
262+
this.boardsDataStore.getData(this.sanitizeFqbn(verifyOptions.fqbn)),
263+
this.preferences.get('arduino.upload.verify'),
264+
this.preferences.get('arduino.upload.verbose'),
265+
]);
266+
const port = boardsConfig.selectedPort;
267+
return {
268+
sketch,
269+
fqbn,
270+
...(usingProgrammer && { programmer }),
271+
port,
272+
verbose,
273+
verify,
274+
userFields,
275+
};
276+
}
277+
278+
private userFields() {
279+
return this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
280+
}
281+
282+
/**
283+
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
284+
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
285+
* See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
286+
*/
287+
private sanitizeFqbn(fqbn: string | undefined): string | undefined {
288+
if (!fqbn) {
289+
return undefined;
290+
}
291+
const [vendor, arch, id] = fqbn.split(':');
292+
return `${vendor}:${arch}:${id}`;
293+
}
298294
}
299295

300296
export namespace UploadSketch {
301297
export namespace Commands {
302298
export const UPLOAD_SKETCH: Command = {
303299
id: 'arduino-upload-sketch',
304300
};
305-
export const UPLOAD_WITH_CONFIGURATION: Command = {
301+
export const UPLOAD_WITH_CONFIGURATION: Command & { label: string } = {
306302
id: 'arduino-upload-with-configuration-sketch',
307303
label: nls.localize(
308304
'arduino/sketch/configureAndUpload',

‎arduino-ide-extension/src/browser/contributions/verify-sketch.ts

Lines changed: 84 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { inject, injectable } from '@theia/core/shared/inversify';
22
import { Emitter } from '@theia/core/lib/common/event';
33
import { ArduinoMenus } from '../menu/arduino-menus';
44
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
5-
import { BoardsDataStore } from '../boards/boards-data-store';
6-
import { BoardsServiceProvider } from '../boards/boards-service-provider';
75
import {
86
CoreServiceContribution,
97
Command,
@@ -14,27 +12,36 @@ import {
1412
} from './contribution';
1513
import { nls } from '@theia/core/lib/common';
1614
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
15+
import { CoreService } from '../../common/protocol';
16+
import { CoreErrorHandler } from './core-error-handler';
17+
18+
export interface VerifySketchParams {
19+
/**
20+
* Same as `CoreService.Options.Compile#exportBinaries`
21+
*/
22+
readonly exportBinaries?: boolean;
23+
/**
24+
* If `true`, there won't be any UI indication of the verify command. It's `false` by default.
25+
*/
26+
readonly silent?: boolean;
27+
}
1728

1829
@injectable()
1930
export class VerifySketch extends CoreServiceContribution {
20-
@inject(BoardsDataStore)
21-
protected readonly boardsDataStore: BoardsDataStore;
22-
23-
@inject(BoardsServiceProvider)
24-
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
31+
@inject(CoreErrorHandler)
32+
private readonly coreErrorHandler: CoreErrorHandler;
2533

26-
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
27-
readonly onDidChange = this.onDidChangeEmitter.event;
28-
29-
protected verifyInProgress = false;
34+
private readonly onDidChangeEmitter = new Emitter<void>();
35+
private readonly onDidChange = this.onDidChangeEmitter.event;
36+
private verifyInProgress = false;
3037

3138
override registerCommands(registry: CommandRegistry): void {
3239
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
33-
execute: () => this.verifySketch(),
40+
execute: (params?: VerifySketchParams) => this.verifySketch(params),
3441
isEnabled: () => !this.verifyInProgress,
3542
});
3643
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
37-
execute: () => this.verifySketch(true),
44+
execute: () => this.verifySketch({ exportBinaries: true }),
3845
isEnabled: () => !this.verifyInProgress,
3946
});
4047
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
@@ -84,61 +91,87 @@ export class VerifySketch extends CoreServiceContribution {
8491
});
8592
}
8693

87-
async verifySketch(exportBinaries?: boolean): Promise<void> {
88-
// even with buttons disabled, better to double check if a verify is already in progress
94+
protected override handleError(error: unknown): void {
95+
this.coreErrorHandler.tryHandle(error);
96+
super.handleError(error);
97+
}
98+
99+
private async verifySketch(
100+
params?: VerifySketchParams
101+
): Promise<CoreService.Options.Compile | undefined> {
89102
if (this.verifyInProgress) {
90-
return;
103+
return undefined;
91104
}
92105

93-
// toggle the toolbar button and menu item state.
94-
// verifyInProgress will be set to false whether the compilation fails or not
95-
const sketch = await this.sketchServiceClient.currentSketch();
96-
if (!CurrentSketch.isValid(sketch)) {
97-
return;
98-
}
99106
try {
100-
this.verifyInProgress = true;
107+
if (!params?.silent) {
108+
this.verifyInProgress = true;
109+
this.onDidChangeEmitter.fire();
110+
}
101111
this.coreErrorHandler.reset();
102-
this.onDidChangeEmitter.fire();
103-
const { boardsConfig } = this.boardsServiceClientImpl;
104-
const [fqbn, sourceOverride] = await Promise.all([
105-
this.boardsDataStore.appendConfigToFqbn(
106-
boardsConfig.selectedBoard?.fqbn
112+
113+
const options = await this.options(params?.exportBinaries);
114+
if (!options) {
115+
return undefined;
116+
}
117+
118+
await this.doWithProgress({
119+
progressText: nls.localize(
120+
'arduino/sketch/compile',
121+
'Compiling sketch...'
107122
),
108-
this.sourceOverride(),
109-
]);
110-
const board = {
111-
...boardsConfig.selectedBoard,
112-
name: boardsConfig.selectedBoard?.name || '',
113-
fqbn,
114-
};
115-
const verbose = this.preferences.get('arduino.compile.verbose');
116-
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
117-
const optimizeForDebug =
118-
await this.commandService.executeCommand<boolean>(
119-
'arduino-is-optimize-for-debug'
120-
);
121-
this.outputChannelManager.getChannel('Arduino').clear();
122-
await this.coreService.compile({
123-
sketch,
124-
board,
125-
optimizeForDebug: Boolean(optimizeForDebug),
126-
verbose,
127-
exportBinaries,
128-
sourceOverride,
129-
compilerWarnings,
123+
task: (progressId, coreService) =>
124+
coreService.compile({
125+
...options,
126+
progressId,
127+
}),
130128
});
131129
this.messageService.info(
132130
nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'),
133131
{ timeout: 3000 }
134132
);
133+
// Returns with the used options for the compilation
134+
// so that follow-up tasks (such as upload) can reuse the compiled code.
135+
// Note that the `fqbn` is already decorated with the board settings, if any.
136+
return options;
135137
} catch (e) {
136138
this.handleError(e);
139+
return undefined;
137140
} finally {
138141
this.verifyInProgress = false;
139-
this.onDidChangeEmitter.fire();
142+
if (!params?.silent) {
143+
this.onDidChangeEmitter.fire();
144+
}
140145
}
141146
}
147+
148+
private async options(
149+
exportBinaries?: boolean
150+
): Promise<CoreService.Options.Compile | undefined> {
151+
const sketch = await this.sketchServiceClient.currentSketch();
152+
if (!CurrentSketch.isValid(sketch)) {
153+
return undefined;
154+
}
155+
const { boardsConfig } = this.boardsServiceProvider;
156+
const [fqbn, sourceOverride, optimizeForDebug] = await Promise.all([
157+
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
158+
this.sourceOverride(),
159+
this.commandService.executeCommand<boolean>(
160+
'arduino-is-optimize-for-debug'
161+
),
162+
]);
163+
const verbose = this.preferences.get('arduino.compile.verbose');
164+
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
165+
return {
166+
sketch,
167+
fqbn,
168+
optimizeForDebug: Boolean(optimizeForDebug),
169+
verbose,
170+
exportBinaries,
171+
sourceOverride,
172+
compilerWarnings,
173+
};
174+
}
142175
}
143176

144177
export namespace VerifySketch {

‎arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CommandService } from '@theia/core/lib/common/command';
55
import { MessageService } from '@theia/core/lib/common/message-service';
66
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
77
import { Searchable } from '../../../common/protocol/searchable';
8+
import { ExecuteWithProgress } from '../../../common/protocol/progressible';
89
import { Installable } from '../../../common/protocol/installable';
910
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
1011
import { SearchBar } from './search-bar';
@@ -111,7 +112,7 @@ export class FilterableListContainer<
111112
version: Installable.Version
112113
): Promise<void> {
113114
const { install, searchable } = this.props;
114-
await Installable.doWithProgress({
115+
await ExecuteWithProgress.doWithProgress({
115116
...this.props,
116117
progressText:
117118
nls.localize('arduino/common/processing', 'Processing') +
@@ -137,7 +138,7 @@ export class FilterableListContainer<
137138
return;
138139
}
139140
const { uninstall, searchable } = this.props;
140-
await Installable.doWithProgress({
141+
await ExecuteWithProgress.doWithProgress({
141142
...this.props,
142143
progressText:
143144
nls.localize('arduino/common/processing', 'Processing') +

‎arduino-ide-extension/src/common/protocol/core-service.ts

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ApplicationError } from '@theia/core/lib/common/application-error';
22
import type { Location } from '@theia/core/shared/vscode-languageserver-protocol';
33
import type {
4-
Board,
54
BoardUserField,
65
Port,
76
} from '../../common/protocol/boards-service';
@@ -60,46 +59,39 @@ export namespace CoreError {
6059
export const CoreServicePath = '/services/core-service';
6160
export const CoreService = Symbol('CoreService');
6261
export interface CoreService {
63-
compile(
64-
options: CoreService.Compile.Options &
65-
Readonly<{
66-
exportBinaries?: boolean;
67-
compilerWarnings?: CompilerWarnings;
68-
}>
69-
): Promise<void>;
70-
upload(options: CoreService.Upload.Options): Promise<void>;
71-
uploadUsingProgrammer(options: CoreService.Upload.Options): Promise<void>;
72-
burnBootloader(options: CoreService.Bootloader.Options): Promise<void>;
62+
compile(options: CoreService.Options.Compile): Promise<void>;
63+
upload(options: CoreService.Options.Upload): Promise<void>;
64+
burnBootloader(options: CoreService.Options.Bootloader): Promise<void>;
7365
}
7466

7567
export namespace CoreService {
76-
export namespace Compile {
77-
export interface Options {
68+
export namespace Options {
69+
export interface Base {
70+
readonly fqbn?: string | undefined;
71+
readonly verbose: boolean; // TODO: (API) why not optional with a default false?
72+
readonly progressId?: string;
73+
}
74+
export interface SketchBased {
7875
readonly sketch: Sketch;
79-
readonly board?: Board;
80-
readonly optimizeForDebug: boolean;
81-
readonly verbose: boolean;
82-
readonly sourceOverride: Record<string, string>;
8376
}
84-
}
85-
86-
export namespace Upload {
87-
export interface Options extends Omit<Compile.Options, 'verbose'> {
77+
export interface BoardBased {
8878
readonly port?: Port;
8979
readonly programmer?: Programmer | undefined;
90-
readonly verify: boolean;
91-
readonly userFields: BoardUserField[];
92-
readonly verbose: { compile: boolean; upload: boolean };
80+
/**
81+
* For the _Verify after upload_ setting.
82+
*/
83+
readonly verify: boolean; // TODO: (API) why not optional with false as the default value?
9384
}
94-
}
95-
96-
export namespace Bootloader {
97-
export interface Options {
98-
readonly board?: Board;
99-
readonly port?: Port;
100-
readonly programmer?: Programmer | undefined;
101-
readonly verbose: boolean;
102-
readonly verify: boolean;
85+
export interface Compile extends Base, SketchBased {
86+
readonly optimizeForDebug: boolean; // TODO: (API) make this optional
87+
readonly sourceOverride: Record<string, string>; // TODO: (API) make this optional
88+
readonly exportBinaries?: boolean;
89+
readonly compilerWarnings?: CompilerWarnings;
90+
}
91+
export interface Upload extends Base, SketchBased, BoardBased {
92+
readonly userFields: BoardUserField[];
93+
readonly usingProgrammer?: boolean;
10394
}
95+
export interface Bootloader extends Base, BoardBased {}
10496
}
10597
}
Lines changed: 5 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import * as semver from 'semver';
2-
import type { Progress } from '@theia/core/lib/common/message-service-protocol';
3-
import {
4-
CancellationToken,
5-
CancellationTokenSource,
6-
} from '@theia/core/lib/common/cancellation';
7-
import { naturalCompare } from './../utils';
2+
import { ExecuteWithProgress } from './progressible';
3+
import { naturalCompare } from '../utils';
84
import type { ArduinoComponent } from './arduino-component';
95
import type { MessageService } from '@theia/core/lib/common/message-service';
106
import type { ResponseServiceClient } from './response-service';
@@ -32,7 +28,7 @@ export namespace Installable {
3228
/**
3329
* Most recent version comes first, then the previous versions. (`1.8.1`, `1.6.3`, `1.6.2`, `1.6.1` and so on.)
3430
*/
35-
export const COMPARATOR = (left: Version, right: Version) => {
31+
export const COMPARATOR = (left: Version, right: Version): number => {
3632
if (semver.valid(left) && semver.valid(right)) {
3733
return semver.compare(left, right);
3834
}
@@ -50,7 +46,7 @@ export namespace Installable {
5046
version: Installable.Version;
5147
}): Promise<void> {
5248
const { item, version } = options;
53-
return doWithProgress({
49+
return ExecuteWithProgress.doWithProgress({
5450
...options,
5551
progressText: `Processing ${item.name}:${version}`,
5652
run: ({ progressId }) =>
@@ -71,7 +67,7 @@ export namespace Installable {
7167
item: T;
7268
}): Promise<void> {
7369
const { item } = options;
74-
return doWithProgress({
70+
return ExecuteWithProgress.doWithProgress({
7571
...options,
7672
progressText: `Processing ${item.name}${
7773
item.installedVersion ? `:${item.installedVersion}` : ''
@@ -83,51 +79,4 @@ export namespace Installable {
8379
}),
8480
});
8581
}
86-
87-
export async function doWithProgress(options: {
88-
run: ({ progressId }: { progressId: string }) => Promise<void>;
89-
messageService: MessageService;
90-
responseService: ResponseServiceClient;
91-
progressText: string;
92-
}): Promise<void> {
93-
return withProgress(
94-
options.progressText,
95-
options.messageService,
96-
async (progress, _) => {
97-
const progressId = progress.id;
98-
const toDispose = options.responseService.onProgressDidChange(
99-
(progressMessage) => {
100-
if (progressId === progressMessage.progressId) {
101-
const { message, work } = progressMessage;
102-
progress.report({ message, work });
103-
}
104-
}
105-
);
106-
try {
107-
options.responseService.clearOutput();
108-
await options.run({ progressId });
109-
} finally {
110-
toDispose.dispose();
111-
}
112-
}
113-
);
114-
}
115-
116-
async function withProgress(
117-
text: string,
118-
messageService: MessageService,
119-
cb: (progress: Progress, token: CancellationToken) => Promise<void>
120-
): Promise<void> {
121-
const cancellationSource = new CancellationTokenSource();
122-
const { token } = cancellationSource;
123-
const progress = await messageService.showProgress(
124-
{ text, options: { cancelable: false } },
125-
() => cancellationSource.cancel()
126-
);
127-
try {
128-
await cb(progress, token);
129-
} finally {
130-
progress.cancel();
131-
}
132-
}
13382
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { CancellationToken } from '@theia/core/lib/common/cancellation';
2+
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
3+
import type { MessageService } from '@theia/core/lib/common/message-service';
4+
import type { Progress } from '@theia/core/lib/common/message-service-protocol';
5+
import type { ResponseServiceClient } from './response-service';
6+
7+
export namespace ExecuteWithProgress {
8+
export async function doWithProgress<T>(options: {
9+
run: ({ progressId }: { progressId: string }) => Promise<T>;
10+
messageService: MessageService;
11+
responseService: ResponseServiceClient;
12+
progressText: string;
13+
keepOutput?: boolean;
14+
}): Promise<T> {
15+
return withProgress(
16+
options.progressText,
17+
options.messageService,
18+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
19+
async (progress, _token) => {
20+
const progressId = progress.id;
21+
const toDispose = options.responseService.onProgressDidChange(
22+
(progressMessage) => {
23+
if (progressId === progressMessage.progressId) {
24+
const { message, work } = progressMessage;
25+
progress.report({ message, work });
26+
}
27+
}
28+
);
29+
try {
30+
if (!options.keepOutput) {
31+
options.responseService.clearOutput();
32+
}
33+
const result = await options.run({ progressId });
34+
return result;
35+
} finally {
36+
toDispose.dispose();
37+
}
38+
}
39+
);
40+
}
41+
42+
async function withProgress<T>(
43+
text: string,
44+
messageService: MessageService,
45+
cb: (progress: Progress, token: CancellationToken) => Promise<T>
46+
): Promise<T> {
47+
const cancellationSource = new CancellationTokenSource();
48+
const { token } = cancellationSource;
49+
const progress = await messageService.showProgress(
50+
{ text, options: { cancelable: false } },
51+
() => cancellationSource.cancel()
52+
);
53+
try {
54+
const result = await cb(progress, token);
55+
return result;
56+
} finally {
57+
progress.cancel();
58+
}
59+
}
60+
}

‎arduino-ide-extension/src/common/protocol/response-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,5 @@ export interface ResponseService {
4646
export const ResponseServiceClient = Symbol('ResponseServiceClient');
4747
export interface ResponseServiceClient extends ResponseService {
4848
onProgressDidChange: Event<ProgressMessage>;
49-
clearOutput: () => void;
49+
clearOutput: () => void; // TODO: this should not belong here.
5050
}

‎arduino-ide-extension/src/node/core-service-impl.ts

Lines changed: 101 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ import { tryParseError } from './cli-error-parser';
3333
import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
3434
import { firstToUpperCase, notEmpty } from '../common/utils';
3535
import { ServiceError } from './service-error';
36+
import { ExecuteWithProgress, ProgressResponse } from './grpc-progressible';
37+
38+
namespace Uploadable {
39+
export type Request = UploadRequest | UploadUsingProgrammerRequest;
40+
export type Response = UploadResponse | UploadUsingProgrammerResponse;
41+
}
3642

3743
@injectable()
3844
export class CoreServiceImpl extends CoreClientAware implements CoreService {
@@ -45,27 +51,27 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
4551
@inject(CommandService)
4652
private readonly commandService: CommandService;
4753

48-
async compile(
49-
options: CoreService.Compile.Options & {
50-
exportBinaries?: boolean;
51-
compilerWarnings?: CompilerWarnings;
52-
}
53-
): Promise<void> {
54+
async compile(options: CoreService.Options.Compile): Promise<void> {
5455
const coreClient = await this.coreClient;
5556
const { client, instance } = coreClient;
5657
let buildPath: string | undefined = undefined;
57-
const handler = this.createOnDataHandler<CompileResponse>((response) => {
58+
const progressHandler = this.createProgressHandler(options);
59+
const buildPathHandler = (response: CompileResponse) => {
5860
const currentBuildPath = response.getBuildPath();
59-
if (!buildPath && currentBuildPath) {
61+
if (currentBuildPath) {
6062
buildPath = currentBuildPath;
6163
} else {
62-
if (!!currentBuildPath && currentBuildPath !== buildPath) {
64+
if (!!buildPath && currentBuildPath !== buildPath) {
6365
throw new Error(
64-
`The CLI has already provided a build path: <${buildPath}>, and there is a new build path value: <${currentBuildPath}>.`
66+
`The CLI has already provided a build path: <${buildPath}>, and IDE2 received a new build path value: <${currentBuildPath}>.`
6567
);
6668
}
6769
}
68-
});
70+
};
71+
const handler = this.createOnDataHandler<CompileResponse>(
72+
progressHandler,
73+
buildPathHandler
74+
);
6975
const request = this.compileRequest(options, instance);
7076
return new Promise<void>((resolve, reject) => {
7177
client
@@ -132,20 +138,20 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
132138
}
133139

134140
private compileRequest(
135-
options: CoreService.Compile.Options & {
141+
options: CoreService.Options.Compile & {
136142
exportBinaries?: boolean;
137143
compilerWarnings?: CompilerWarnings;
138144
},
139145
instance: Instance
140146
): CompileRequest {
141-
const { sketch, board, compilerWarnings } = options;
147+
const { sketch, fqbn, compilerWarnings } = options;
142148
const sketchUri = sketch.uri;
143149
const sketchPath = FileUri.fsPath(sketchUri);
144150
const request = new CompileRequest();
145151
request.setInstance(instance);
146152
request.setSketchPath(sketchPath);
147-
if (board?.fqbn) {
148-
request.setFqbn(board.fqbn);
153+
if (fqbn) {
154+
request.setFqbn(fqbn);
149155
}
150156
if (compilerWarnings) {
151157
request.setWarnings(compilerWarnings.toLowerCase());
@@ -163,60 +169,44 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
163169
return request;
164170
}
165171

166-
upload(options: CoreService.Upload.Options): Promise<void> {
167-
return this.doUpload(
168-
options,
169-
() => new UploadRequest(),
170-
(client, req) => client.upload(req),
171-
(message: string, locations: CoreError.ErrorLocation[]) =>
172-
CoreError.UploadFailed(message, locations),
173-
'upload'
174-
);
175-
}
176-
177-
async uploadUsingProgrammer(
178-
options: CoreService.Upload.Options
179-
): Promise<void> {
172+
upload(options: CoreService.Options.Upload): Promise<void> {
173+
const { usingProgrammer } = options;
180174
return this.doUpload(
181175
options,
182-
() => new UploadUsingProgrammerRequest(),
183-
(client, req) => client.uploadUsingProgrammer(req),
184-
(message: string, locations: CoreError.ErrorLocation[]) =>
185-
CoreError.UploadUsingProgrammerFailed(message, locations),
186-
'upload using programmer'
176+
usingProgrammer
177+
? new UploadUsingProgrammerRequest()
178+
: new UploadRequest(),
179+
(client) =>
180+
(usingProgrammer ? client.uploadUsingProgrammer : client.upload).bind(
181+
client
182+
),
183+
usingProgrammer
184+
? CoreError.UploadUsingProgrammerFailed
185+
: CoreError.UploadFailed,
186+
`upload${usingProgrammer ? ' using programmer' : ''}`
187187
);
188188
}
189189

190-
protected async doUpload(
191-
options: CoreService.Upload.Options,
192-
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest,
193-
responseHandler: (
194-
client: ArduinoCoreServiceClient,
195-
request: UploadRequest | UploadUsingProgrammerRequest
196-
) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
197-
errorHandler: (
198-
message: string,
199-
locations: CoreError.ErrorLocation[]
200-
) => ApplicationError<number, CoreError.ErrorLocation[]>,
190+
protected async doUpload<
191+
REQ extends Uploadable.Request,
192+
RESP extends Uploadable.Response
193+
>(
194+
options: CoreService.Options.Upload,
195+
request: REQ,
196+
responseFactory: (
197+
client: ArduinoCoreServiceClient
198+
) => (request: REQ) => ClientReadableStream<RESP>,
199+
errorCtor: ApplicationError.Constructor<number, CoreError.ErrorLocation[]>,
201200
task: string
202201
): Promise<void> {
203-
await this.compile({
204-
...options,
205-
verbose: options.verbose.compile,
206-
exportBinaries: false,
207-
});
208-
209202
const coreClient = await this.coreClient;
210203
const { client, instance } = coreClient;
211-
const request = this.uploadOrUploadUsingProgrammerRequest(
212-
options,
213-
instance,
214-
requestFactory
215-
);
216-
const handler = this.createOnDataHandler();
204+
const progressHandler = this.createProgressHandler(options);
205+
const handler = this.createOnDataHandler(progressHandler);
206+
const grpcCall = responseFactory(client);
217207
return this.notifyUploadWillStart(options).then(() =>
218208
new Promise<void>((resolve, reject) => {
219-
responseHandler(client, request)
209+
grpcCall(this.initUploadRequest(request, options, instance))
220210
.on('data', handler.onData)
221211
.on('error', (error) => {
222212
if (!ServiceError.is(error)) {
@@ -231,7 +221,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
231221
);
232222
this.sendResponse(error.details, OutputMessage.Severity.Error);
233223
reject(
234-
errorHandler(
224+
errorCtor(
235225
message,
236226
tryParseError({
237227
content: handler.stderr,
@@ -249,24 +239,23 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
249239
);
250240
}
251241

252-
private uploadOrUploadUsingProgrammerRequest(
253-
options: CoreService.Upload.Options,
254-
instance: Instance,
255-
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest
256-
): UploadRequest | UploadUsingProgrammerRequest {
257-
const { sketch, board, port, programmer } = options;
242+
private initUploadRequest<REQ extends Uploadable.Request>(
243+
request: REQ,
244+
options: CoreService.Options.Upload,
245+
instance: Instance
246+
): REQ {
247+
const { sketch, fqbn, port, programmer } = options;
258248
const sketchPath = FileUri.fsPath(sketch.uri);
259-
const request = requestFactory();
260249
request.setInstance(instance);
261250
request.setSketchPath(sketchPath);
262-
if (board?.fqbn) {
263-
request.setFqbn(board.fqbn);
251+
if (fqbn) {
252+
request.setFqbn(fqbn);
264253
}
265254
request.setPort(this.createPort(port));
266255
if (programmer) {
267256
request.setProgrammer(programmer.id);
268257
}
269-
request.setVerbose(options.verbose.upload);
258+
request.setVerbose(options.verbose);
270259
request.setVerify(options.verify);
271260

272261
options.userFields.forEach((e) => {
@@ -275,10 +264,11 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
275264
return request;
276265
}
277266

278-
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
267+
async burnBootloader(options: CoreService.Options.Bootloader): Promise<void> {
279268
const coreClient = await this.coreClient;
280269
const { client, instance } = coreClient;
281-
const handler = this.createOnDataHandler();
270+
const progressHandler = this.createProgressHandler(options);
271+
const handler = this.createOnDataHandler(progressHandler);
282272
const request = this.burnBootloaderRequest(options, instance);
283273
return this.notifyUploadWillStart(options).then(() =>
284274
new Promise<void>((resolve, reject) => {
@@ -315,14 +305,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
315305
}
316306

317307
private burnBootloaderRequest(
318-
options: CoreService.Bootloader.Options,
308+
options: CoreService.Options.Bootloader,
319309
instance: Instance
320310
): BurnBootloaderRequest {
321-
const { board, port, programmer } = options;
311+
const { fqbn, port, programmer } = options;
322312
const request = new BurnBootloaderRequest();
323313
request.setInstance(instance);
324-
if (board?.fqbn) {
325-
request.setFqbn(board.fqbn);
314+
if (fqbn) {
315+
request.setFqbn(fqbn);
326316
}
327317
request.setPort(this.createPort(port));
328318
if (programmer) {
@@ -333,8 +323,24 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
333323
return request;
334324
}
335325

326+
private createProgressHandler<R extends ProgressResponse>(
327+
options: CoreService.Options.Base
328+
): (response: R) => void {
329+
// If client did not provide the progress ID, do nothing.
330+
if (!options.progressId) {
331+
return () => {
332+
/* NOOP */
333+
};
334+
}
335+
return ExecuteWithProgress.createDataCallback<R>({
336+
progressId: options.progressId,
337+
responseService: this.responseService,
338+
});
339+
}
340+
336341
private createOnDataHandler<R extends StreamingResponse>(
337-
onResponse?: (response: R) => void
342+
// TODO: why not creating a composite handler with progress, `build_path`, and out/err stream handlers?
343+
...handlers: ((response: R) => void)[]
338344
): Disposable & {
339345
stderr: Buffer[];
340346
onData: (response: R) => void;
@@ -347,14 +353,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
347353
}
348354
});
349355
});
350-
const onData = StreamingResponse.createOnDataHandler(
356+
const onData = StreamingResponse.createOnDataHandler({
351357
stderr,
352-
(out, err) => {
358+
onData: (out, err) => {
353359
buffer.addChunk(out);
354360
buffer.addChunk(err, OutputMessage.Severity.Error);
355361
},
356-
onResponse
357-
);
362+
handlers,
363+
});
358364
return {
359365
dispose: () => buffer.dispose(),
360366
stderr,
@@ -391,7 +397,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
391397

392398
private mergeSourceOverrides(
393399
req: { getSourceOverrideMap(): jspb.Map<string, string> },
394-
options: CoreService.Compile.Options
400+
options: CoreService.Options.Compile
395401
): void {
396402
const sketchPath = FileUri.fsPath(options.sketch.uri);
397403
for (const uri of Object.keys(options.sourceOverride)) {
@@ -422,18 +428,24 @@ type StreamingResponse =
422428
namespace StreamingResponse {
423429
// eslint-disable-next-line @typescript-eslint/no-explicit-any
424430
export function createOnDataHandler<R extends StreamingResponse>(
425-
stderr: Uint8Array[],
426-
onData: (out: Uint8Array, err: Uint8Array) => void,
427-
onResponse?: (response: R) => void
431+
options: StreamingResponse.Options<R>
428432
): (response: R) => void {
429433
return (response: R) => {
430434
const out = response.getOutStream_asU8();
431435
const err = response.getErrStream_asU8();
432-
stderr.push(err);
433-
onData(out, err);
434-
if (onResponse) {
435-
onResponse(response);
436-
}
436+
options.stderr.push(err);
437+
options.onData(out, err);
438+
options.handlers?.forEach((handler) => handler(response));
437439
};
438440
}
441+
export interface Options<R extends StreamingResponse> {
442+
readonly stderr: Uint8Array[];
443+
readonly onData: (out: Uint8Array, err: Uint8Array) => void;
444+
/**
445+
* Additional request handlers.
446+
* For example, when tracing the progress of a task and
447+
* collecting the output (out, err) and the `build_path` from the CLI.
448+
*/
449+
readonly handlers?: ((response: R) => void)[];
450+
}
439451
}

‎arduino-ide-extension/src/node/grpc-progressible.ts

Lines changed: 102 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
DownloadProgress,
1313
TaskProgress,
1414
} from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
15+
import { CompileResponse } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb';
1516
import {
1617
PlatformInstallResponse,
1718
PlatformUninstallResponse,
@@ -21,6 +22,11 @@ import {
2122
LibraryUninstallResponse,
2223
ZipLibraryInstallResponse,
2324
} from './cli-protocol/cc/arduino/cli/commands/v1/lib_pb';
25+
import {
26+
BurnBootloaderResponse,
27+
UploadResponse,
28+
UploadUsingProgrammerResponse,
29+
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
2430

2531
type LibraryProgressResponse =
2632
| LibraryInstallResponse
@@ -78,15 +84,62 @@ namespace IndexProgressResponse {
7884
return { download: response.getDownloadProgress() };
7985
}
8086
}
87+
/**
88+
* These responses have neither `task` nor `progress` property but for the sake of completeness
89+
* on typings (from the gRPC API) and UX, these responses represent an indefinite progress.
90+
*/
91+
type IndefiniteProgressResponse =
92+
| UploadResponse
93+
| UploadUsingProgrammerResponse
94+
| BurnBootloaderResponse;
95+
namespace IndefiniteProgressResponse {
96+
export function is(
97+
response: unknown
98+
): response is IndefiniteProgressResponse {
99+
return (
100+
response instanceof UploadResponse ||
101+
response instanceof UploadUsingProgrammerResponse ||
102+
response instanceof BurnBootloaderResponse
103+
);
104+
}
105+
}
106+
type DefiniteProgressResponse = CompileResponse;
107+
namespace DefiniteProgressResponse {
108+
export function is(response: unknown): response is DefiniteProgressResponse {
109+
return response instanceof CompileResponse;
110+
}
111+
}
112+
type CoreProgressResponse =
113+
| DefiniteProgressResponse
114+
| IndefiniteProgressResponse;
115+
namespace CoreProgressResponse {
116+
export function is(response: unknown): response is CoreProgressResponse {
117+
return (
118+
DefiniteProgressResponse.is(response) ||
119+
IndefiniteProgressResponse.is(response)
120+
);
121+
}
122+
export function workUnit(response: CoreProgressResponse): UnitOfWork {
123+
if (DefiniteProgressResponse.is(response)) {
124+
return { task: response.getProgress() };
125+
}
126+
return UnitOfWork.Unknown;
127+
}
128+
}
129+
81130
export type ProgressResponse =
82131
| LibraryProgressResponse
83132
| PlatformProgressResponse
84-
| IndexProgressResponse;
133+
| IndexProgressResponse
134+
| CoreProgressResponse;
85135

86136
interface UnitOfWork {
87137
task?: TaskProgress;
88138
download?: DownloadProgress;
89139
}
140+
namespace UnitOfWork {
141+
export const Unknown: UnitOfWork = {};
142+
}
90143

91144
/**
92145
* 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 {
115168
console.log(`Progress response [${uuid}]: ${json}`);
116169
}
117170
}
118-
const { task, download } = resolve(response);
171+
const unitOfWork = resolve(response);
172+
const { task, download } = unitOfWork;
119173
if (!download && !task) {
120-
console.warn(
121-
"Implementation error. Neither 'download' nor 'task' is available."
122-
);
123-
// This is still an API error from the CLI, but IDE2 ignores it.
124-
// Technically, it does not cause an error, but could mess up the progress reporting.
125-
// See an example of an empty object `{}` repose here: https://github.com/arduino/arduino-ide/issues/906#issuecomment-1171145630.
174+
// report a fake unknown progress.
175+
if (unitOfWork === UnitOfWork.Unknown && progressId) {
176+
if (progressId) {
177+
responseService.reportProgress?.({
178+
progressId,
179+
message: '',
180+
work: { done: Number.NaN, total: Number.NaN },
181+
});
182+
}
183+
return;
184+
}
185+
if (DEBUG) {
186+
// This is still an API error from the CLI, but IDE2 ignores it.
187+
// Technically, it does not cause an error, but could mess up the progress reporting.
188+
// See an example of an empty object `{}` repose here: https://github.com/arduino/arduino-ide/issues/906#issuecomment-1171145630.
189+
console.warn(
190+
"Implementation error. Neither 'download' nor 'task' is available."
191+
);
192+
}
126193
return;
127194
}
128195
if (task && download) {
@@ -132,6 +199,7 @@ export namespace ExecuteWithProgress {
132199
}
133200
if (task) {
134201
const message = task.getName() || task.getMessage();
202+
const percent = task.getPercent();
135203
if (message) {
136204
if (progressId) {
137205
responseService.reportProgress?.({
@@ -141,6 +209,14 @@ export namespace ExecuteWithProgress {
141209
});
142210
}
143211
responseService.appendToOutput?.({ chunk: `${message}\n` });
212+
} else if (percent) {
213+
if (progressId) {
214+
responseService.reportProgress?.({
215+
progressId,
216+
message,
217+
work: { done: percent, total: 100 },
218+
});
219+
}
144220
}
145221
} else if (download) {
146222
if (download.getFile() && !localFile) {
@@ -191,38 +267,38 @@ export namespace ExecuteWithProgress {
191267
return PlatformProgressResponse.workUnit(response);
192268
} else if (IndexProgressResponse.is(response)) {
193269
return IndexProgressResponse.workUnit(response);
270+
} else if (CoreProgressResponse.is(response)) {
271+
return CoreProgressResponse.workUnit(response);
194272
}
195273
console.warn('Unhandled gRPC response', response);
196274
return {};
197275
}
198276
function toJson(response: ProgressResponse): string | undefined {
277+
let object: Record<string, unknown> | undefined = undefined;
199278
if (response instanceof LibraryInstallResponse) {
200-
return JSON.stringify(LibraryInstallResponse.toObject(false, response));
279+
object = LibraryInstallResponse.toObject(false, response);
201280
} else if (response instanceof LibraryUninstallResponse) {
202-
return JSON.stringify(LibraryUninstallResponse.toObject(false, response));
281+
object = LibraryUninstallResponse.toObject(false, response);
203282
} else if (response instanceof ZipLibraryInstallResponse) {
204-
return JSON.stringify(
205-
ZipLibraryInstallResponse.toObject(false, response)
206-
);
283+
object = ZipLibraryInstallResponse.toObject(false, response);
207284
} else if (response instanceof PlatformInstallResponse) {
208-
return JSON.stringify(PlatformInstallResponse.toObject(false, response));
285+
object = PlatformInstallResponse.toObject(false, response);
209286
} else if (response instanceof PlatformUninstallResponse) {
210-
return JSON.stringify(
211-
PlatformUninstallResponse.toObject(false, response)
212-
);
287+
object = PlatformUninstallResponse.toObject(false, response);
213288
} else if (response instanceof UpdateIndexResponse) {
214-
return JSON.stringify(UpdateIndexResponse.toObject(false, response));
289+
object = UpdateIndexResponse.toObject(false, response);
215290
} else if (response instanceof UpdateLibrariesIndexResponse) {
216-
return JSON.stringify(
217-
UpdateLibrariesIndexResponse.toObject(false, response)
218-
);
291+
object = UpdateLibrariesIndexResponse.toObject(false, response);
219292
} else if (response instanceof UpdateCoreLibrariesIndexResponse) {
220-
return JSON.stringify(
221-
UpdateCoreLibrariesIndexResponse.toObject(false, response)
222-
);
293+
object = UpdateCoreLibrariesIndexResponse.toObject(false, response);
294+
} else if (response instanceof CompileResponse) {
295+
object = CompileResponse.toObject(false, response);
223296
}
224-
console.warn('Unhandled gRPC response', response);
225-
return undefined;
297+
if (!object) {
298+
console.warn('Unhandled gRPC response', response);
299+
return undefined;
300+
}
301+
return JSON.stringify(object);
226302
}
227303
}
228304

‎i18n/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"boardsManager": "Boards Manager",
3737
"bootloader": {
3838
"burnBootloader": "Burn Bootloader",
39+
"burningBootloader": "Burning bootloader...",
3940
"doneBurningBootloader": "Done burning bootloader."
4041
},
4142
"burnBootloader": {
@@ -306,6 +307,7 @@
306307
"archiveSketch": "Archive Sketch",
307308
"cantOpen": "A folder named \"{0}\" already exists. Can't open sketch.",
308309
"close": "Are you sure you want to close the sketch?",
310+
"compile": "Compiling sketch...",
309311
"configureAndUpload": "Configure And Upload",
310312
"createdArchive": "Created archive '{0}'.",
311313
"doneCompiling": "Done compiling.",
@@ -327,6 +329,7 @@
327329
"titleSketchbook": "Sketchbook",
328330
"upload": "Upload",
329331
"uploadUsingProgrammer": "Upload Using Programmer",
332+
"uploading": "Uploading...",
330333
"userFieldsNotFoundError": "Can't find user fields for connected board",
331334
"verify": "Verify",
332335
"verifyOrCompile": "Verify/Compile"

0 commit comments

Comments
 (0)
Please sign in to comment.