diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 90f97bfa0..dd83b4d2b 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -158,7 +158,11 @@ ], "arduino": { "cli": { - "version": "0.27.1" + "version": { + "owner": "cmaglie", + "repo": "arduino-cli", + "commitish": "download_progress_refactor" + } }, "fwuploader": { "version": "2.2.0" diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 0a6df55ff..afa5eb5ec 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -332,6 +332,7 @@ import { OutputEditorFactory } from './theia/output/output-editor-factory'; import { StartupTaskProvider } from '../electron-common/startup-task'; import { DeleteSketch } from './contributions/delete-sketch'; import { UserFields } from './contributions/user-fields'; +import { UpdateIndexes } from './contributions/update-indexes'; const registerArduinoThemes = () => { const themes: MonacoThemeJson[] = [ @@ -744,6 +745,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { Contribution.configure(bind, CheckForUpdates); Contribution.configure(bind, UserFields); Contribution.configure(bind, DeleteSketch); + Contribution.configure(bind, UpdateIndexes); bindContributionProvider(bind, StartupTaskProvider); bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window diff --git a/arduino-ide-extension/src/browser/boards/boards-config.tsx b/arduino-ide-extension/src/browser/boards/boards-config.tsx index df5ed5a33..a4533fdf2 100644 --- a/arduino-ide-extension/src/browser/boards/boards-config.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-config.tsx @@ -132,7 +132,7 @@ export class BoardsConfig extends React.Component< this.props.notificationCenter.onPlatformDidUninstall(() => this.updateBoards(this.state.query) ), - this.props.notificationCenter.onIndexDidUpdate(() => + this.props.notificationCenter.onIndexUpdateDidComplete(() => this.updateBoards(this.state.query) ), this.props.notificationCenter.onDaemonDidStart(() => diff --git a/arduino-ide-extension/src/browser/contributions/indexes-update-progress.ts b/arduino-ide-extension/src/browser/contributions/indexes-update-progress.ts index d8762b841..a2c87fee0 100644 --- a/arduino-ide-extension/src/browser/contributions/indexes-update-progress.ts +++ b/arduino-ide-extension/src/browser/contributions/indexes-update-progress.ts @@ -16,7 +16,7 @@ export class IndexesUpdateProgress extends Contribution { | undefined; override onStart(): void { - this.notificationCenter.onIndexWillUpdate((progressId) => + this.notificationCenter.onIndexUpdateWillStart(({ progressId }) => this.getOrCreateProgress(progressId) ); this.notificationCenter.onIndexUpdateDidProgress((progress) => { @@ -24,7 +24,7 @@ export class IndexesUpdateProgress extends Contribution { delegate.report(progress) ); }); - this.notificationCenter.onIndexDidUpdate((progressId) => { + this.notificationCenter.onIndexUpdateDidComplete(({ progressId }) => { this.cancelProgress(progressId); }); this.notificationCenter.onIndexUpdateDidFail(({ progressId, message }) => { diff --git a/arduino-ide-extension/src/browser/contributions/update-indexes.ts b/arduino-ide-extension/src/browser/contributions/update-indexes.ts new file mode 100644 index 000000000..fc4e6d078 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/update-indexes.ts @@ -0,0 +1,193 @@ +import { LocalStorageService } from '@theia/core/lib/browser/storage-service'; +import { nls } from '@theia/core/lib/common/nls'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { CoreService, IndexType } from '../../common/protocol'; +import { NotificationCenter } from '../notification-center'; +import { WindowServiceExt } from '../theia/core/window-service-ext'; +import { Command, CommandRegistry, Contribution } from './contribution'; + +@injectable() +export class UpdateIndexes extends Contribution { + @inject(WindowServiceExt) + private readonly windowService: WindowServiceExt; + @inject(LocalStorageService) + private readonly localStorage: LocalStorageService; + @inject(CoreService) + private readonly coreService: CoreService; + @inject(NotificationCenter) + private readonly notificationCenter: NotificationCenter; + + protected override init(): void { + super.init(); + this.notificationCenter.onIndexUpdateDidComplete(({ summary }) => + Promise.all( + Object.entries(summary).map(([type, updatedAt]) => + this.setLastUpdateDateTime(type as IndexType, updatedAt) + ) + ) + ); + } + + override onReady(): void { + this.checkForUpdates(); + } + + override registerCommands(registry: CommandRegistry): void { + registry.registerCommand(UpdateIndexes.Commands.UPDATE_INDEXES, { + execute: () => this.updateIndexes(IndexType.All, true), + }); + registry.registerCommand(UpdateIndexes.Commands.UPDATE_PLATFORM_INDEX, { + execute: () => this.updateIndexes(['platform'], true), + }); + registry.registerCommand(UpdateIndexes.Commands.UPDATE_LIBRARY_INDEX, { + execute: () => this.updateIndexes(['library'], true), + }); + } + + private async checkForUpdates(): Promise { + const checkForUpdates = this.preferences['arduino.checkForUpdates']; + if (!checkForUpdates) { + console.debug( + '[update-indexes]: `arduino.checkForUpdates` is `false`. Skipping updating the indexes.' + ); + return; + } + + if (await this.windowService.isFirstWindow()) { + const summary = await this.coreService.indexUpdateSummaryBeforeInit(); + if (summary.message) { + this.messageService.error(summary.message); + } + const typesToCheck = IndexType.All.filter((type) => !(type in summary)); + if (Object.keys(summary).length) { + console.debug( + `[update-indexes]: Detected an index update summary before the core gRPC client initialization. Updating local storage with ${JSON.stringify( + summary + )}` + ); + } else { + console.debug( + '[update-indexes]: No index update summary was available before the core gRPC client initialization. Checking the status of the all the index types.' + ); + } + await Promise.allSettled([ + ...Object.entries(summary).map(([type, updatedAt]) => + this.setLastUpdateDateTime(type as IndexType, updatedAt) + ), + this.updateIndexes(typesToCheck), + ]); + } + } + + private async updateIndexes( + types: IndexType[], + force = false + ): Promise { + const updatedAt = new Date().toISOString(); + return Promise.all( + types.map((type) => this.needsIndexUpdate(type, updatedAt, force)) + ).then((needsIndexUpdateResults) => { + const typesToUpdate = needsIndexUpdateResults.filter(IndexType.is); + if (typesToUpdate.length) { + console.debug( + `[update-indexes]: Requesting the index update of type: ${JSON.stringify( + typesToUpdate + )} with date time: ${updatedAt}.` + ); + return this.coreService.updateIndex({ types: typesToUpdate }); + } + }); + } + + private async needsIndexUpdate( + type: IndexType, + now: string, + force = false + ): Promise { + if (force) { + console.debug( + `[update-indexes]: Update for index type: '${type}' was forcefully requested.` + ); + return type; + } + const lastUpdateIsoDateTime = await this.getLastUpdateDateTime(type); + if (!lastUpdateIsoDateTime) { + console.debug( + `[update-indexes]: No last update date time was persisted for index type: '${type}'. Index update is required.` + ); + return type; + } + const lastUpdateDateTime = Date.parse(lastUpdateIsoDateTime); + if (Number.isNaN(lastUpdateDateTime)) { + console.debug( + `[update-indexes]: Invalid last update date time was persisted for index type: '${type}'. Last update date time was: ${lastUpdateDateTime}. Index update is required.` + ); + return type; + } + const diff = new Date(now).getTime() - lastUpdateDateTime; + const needsIndexUpdate = diff >= this.threshold; + console.debug( + `[update-indexes]: Update for index type '${type}' is ${ + needsIndexUpdate ? '' : 'not ' + }required. Now: ${now}, Last index update date time: ${new Date( + lastUpdateDateTime + ).toISOString()}, diff: ${diff} ms, threshold: ${this.threshold} ms.` + ); + return needsIndexUpdate ? type : false; + } + + private async getLastUpdateDateTime( + type: IndexType + ): Promise { + const key = this.storageKeyOf(type); + return this.localStorage.getData(key); + } + + private async setLastUpdateDateTime( + type: IndexType, + updatedAt: string + ): Promise { + const key = this.storageKeyOf(type); + return this.localStorage.setData(key, updatedAt).finally(() => { + console.debug( + `[update-indexes]: Updated the last index update date time of '${type}' to ${updatedAt}.` + ); + }); + } + + private storageKeyOf(type: IndexType): string { + return `index-last-update-time--${type}`; + } + + private get threshold(): number { + return 4 * 60 * 60 * 1_000; // four hours in millis + } +} +export namespace UpdateIndexes { + export namespace Commands { + export const UPDATE_INDEXES: Command & { label: string } = { + id: 'arduino-update-indexes', + label: nls.localize( + 'arduino/updateIndexes/updateIndexes', + 'Update Indexes' + ), + category: 'Arduino', + }; + export const UPDATE_PLATFORM_INDEX: Command & { label: string } = { + id: 'arduino-update-package-index', + label: nls.localize( + 'arduino/updateIndexes/updatePackageIndex', + 'Update Package Index' + ), + category: 'Arduino', + }; + export const UPDATE_LIBRARY_INDEX: Command & { label: string } = { + id: 'arduino-update-library-index', + label: nls.localize( + 'arduino/updateIndexes/updateLibraryIndex', + 'Update Library Index' + ), + category: 'Arduino', + }; + } +} diff --git a/arduino-ide-extension/src/browser/notification-center.ts b/arduino-ide-extension/src/browser/notification-center.ts index b17853d0a..091ad8cdb 100644 --- a/arduino-ide-extension/src/browser/notification-center.ts +++ b/arduino-ide-extension/src/browser/notification-center.ts @@ -8,6 +8,9 @@ import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { + IndexUpdateDidCompleteParams, + IndexUpdateDidFailParams, + IndexUpdateWillStartParams, NotificationServiceClient, NotificationServiceServer, } from '../common/protocol/notification-service'; @@ -29,48 +32,48 @@ export class NotificationCenter implements NotificationServiceClient, FrontendApplicationContribution { @inject(NotificationServiceServer) - protected readonly server: JsonRpcProxy; + private readonly server: JsonRpcProxy; @inject(FrontendApplicationStateService) private readonly appStateService: FrontendApplicationStateService; - protected readonly indexDidUpdateEmitter = new Emitter(); - protected readonly indexWillUpdateEmitter = new Emitter(); - protected readonly indexUpdateDidProgressEmitter = + private readonly indexUpdateDidCompleteEmitter = + new Emitter(); + private readonly indexUpdateWillStartEmitter = + new Emitter(); + private readonly indexUpdateDidProgressEmitter = new Emitter(); - protected readonly indexUpdateDidFailEmitter = new Emitter<{ - progressId: string; - message: string; - }>(); - protected readonly daemonDidStartEmitter = new Emitter(); - protected readonly daemonDidStopEmitter = new Emitter(); - protected readonly configDidChangeEmitter = new Emitter<{ + private readonly indexUpdateDidFailEmitter = + new Emitter(); + private readonly daemonDidStartEmitter = new Emitter(); + private readonly daemonDidStopEmitter = new Emitter(); + private readonly configDidChangeEmitter = new Emitter<{ config: Config | undefined; }>(); - protected readonly platformDidInstallEmitter = new Emitter<{ + private readonly platformDidInstallEmitter = new Emitter<{ item: BoardsPackage; }>(); - protected readonly platformDidUninstallEmitter = new Emitter<{ + private readonly platformDidUninstallEmitter = new Emitter<{ item: BoardsPackage; }>(); - protected readonly libraryDidInstallEmitter = new Emitter<{ + private readonly libraryDidInstallEmitter = new Emitter<{ item: LibraryPackage; }>(); - protected readonly libraryDidUninstallEmitter = new Emitter<{ + private readonly libraryDidUninstallEmitter = new Emitter<{ item: LibraryPackage; }>(); - protected readonly attachedBoardsDidChangeEmitter = + private readonly attachedBoardsDidChangeEmitter = new Emitter(); - protected readonly recentSketchesChangedEmitter = new Emitter<{ + private readonly recentSketchesChangedEmitter = new Emitter<{ sketches: Sketch[]; }>(); private readonly onAppStateDidChangeEmitter = new Emitter(); - protected readonly toDispose = new DisposableCollection( - this.indexWillUpdateEmitter, + private readonly toDispose = new DisposableCollection( + this.indexUpdateWillStartEmitter, this.indexUpdateDidProgressEmitter, - this.indexDidUpdateEmitter, + this.indexUpdateDidCompleteEmitter, this.indexUpdateDidFailEmitter, this.daemonDidStartEmitter, this.daemonDidStopEmitter, @@ -82,8 +85,8 @@ export class NotificationCenter this.attachedBoardsDidChangeEmitter ); - readonly onIndexDidUpdate = this.indexDidUpdateEmitter.event; - readonly onIndexWillUpdate = this.indexDidUpdateEmitter.event; + readonly onIndexUpdateDidComplete = this.indexUpdateDidCompleteEmitter.event; + readonly onIndexUpdateWillStart = this.indexUpdateWillStartEmitter.event; readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event; readonly onIndexUpdateDidFail = this.indexUpdateDidFailEmitter.event; readonly onDaemonDidStart = this.daemonDidStartEmitter.event; @@ -112,26 +115,20 @@ export class NotificationCenter this.toDispose.dispose(); } - notifyIndexWillUpdate(progressId: string): void { - this.indexWillUpdateEmitter.fire(progressId); + notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void { + this.indexUpdateWillStartEmitter.fire(params); } notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void { this.indexUpdateDidProgressEmitter.fire(progressMessage); } - notifyIndexDidUpdate(progressId: string): void { - this.indexDidUpdateEmitter.fire(progressId); + notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void { + this.indexUpdateDidCompleteEmitter.fire(params); } - notifyIndexUpdateDidFail({ - progressId, - message, - }: { - progressId: string; - message: string; - }): void { - this.indexUpdateDidFailEmitter.fire({ progressId, message }); + notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void { + this.indexUpdateDidFailEmitter.fire(params); } notifyDaemonDidStart(port: string): void { diff --git a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx index 3e388cce2..7e81c9b40 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx @@ -68,7 +68,7 @@ export abstract class ListWidget< @postConstruct() protected init(): void { this.toDispose.pushAll([ - this.notificationCenter.onIndexDidUpdate(() => this.refresh(undefined)), + this.notificationCenter.onIndexUpdateDidComplete(() => this.refresh(undefined)), this.notificationCenter.onDaemonDidStart(() => this.refresh(undefined)), this.notificationCenter.onDaemonDidStop(() => this.refresh(undefined)), ]); diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index 81e5212a9..809f3a7c6 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -11,6 +11,7 @@ import type { } from '../../common/protocol/boards-service'; import type { Programmer } from './boards-service'; import type { Sketch } from './sketches-service'; +import { IndexUpdateSummary } from './notification-service'; export const CompilerWarningLiterals = [ 'None', @@ -112,6 +113,33 @@ export interface CoreService { * Refreshes the underling core gRPC client for the Arduino CLI. */ refresh(): Promise; + /** + * Updates the index of the given index types and refreshes (`init`) the underlying core gRPC client. + * If `types` is empty, only the refresh part will be executed. + */ + updateIndex({ types }: { types: IndexType[] }): Promise; + /** + * If the IDE2 detects invalid or missing indexes on core client init, + * IDE2 tries to update the indexes before the first frontend connects. + * Use this method to determine whether the backend has already updated + * the indexes before updating them. + * + * If yes, the connected frontend can update the local storage with the most + * recent index update date-time for a particular index type, + * and IDE2 can avoid the double indexes update. + */ + indexUpdateSummaryBeforeInit(): Promise>; +} + +export const IndexTypeLiterals = ['platform', 'library'] as const; +export type IndexType = typeof IndexTypeLiterals[number]; +export namespace IndexType { + export function is(arg: unknown): arg is IndexType { + return ( + typeof arg === 'string' && IndexTypeLiterals.includes(arg as IndexType) + ); + } + export const All: IndexType[] = IndexTypeLiterals.filter(is); } export namespace CoreService { diff --git a/arduino-ide-extension/src/common/protocol/notification-service.ts b/arduino-ide-extension/src/common/protocol/notification-service.ts index e1b192ece..cbea74b60 100644 --- a/arduino-ide-extension/src/common/protocol/notification-service.ts +++ b/arduino-ide-extension/src/common/protocol/notification-service.ts @@ -5,27 +5,62 @@ import type { Config, ProgressMessage, Sketch, + IndexType, } from '../protocol'; import type { LibraryPackage } from './library-service'; +/** + * Values are [ISO 8601](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) + * strings representing the date-time when the update of the index has been completed. + */ +export type IndexUpdateSummary = { + [T in IndexType]: string; +} & { message?: string }; +export interface IndexUpdateParams { + /** + * Application unique ID of the progress. + */ + readonly progressId: string; + /** + * The type of the index is which is being updated. + */ + readonly types: IndexType[]; +} +export type IndexUpdateWillStartParams = IndexUpdateParams; +export interface IndexUpdateDidCompleteParams + extends Omit { + readonly summary: IndexUpdateSummary; +} +export interface IndexUpdateDidFailParams extends IndexUpdateParams { + /** + * Describes the reason of the index update failure. + */ + readonly message: string; +} + export interface NotificationServiceClient { - notifyIndexWillUpdate(progressId: string): void; + // Index + notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void; notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void; - notifyIndexDidUpdate(progressId: string): void; - notifyIndexUpdateDidFail({ - progressId, - message, - }: { - progressId: string; - message: string; - }): void; + notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void; + notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void; + + // Daemon notifyDaemonDidStart(port: string): void; notifyDaemonDidStop(): void; + + // CLI config notifyConfigDidChange(event: { config: Config | undefined }): void; + + // Platforms notifyPlatformDidInstall(event: { item: BoardsPackage }): void; notifyPlatformDidUninstall(event: { item: BoardsPackage }): void; + + // Libraries notifyLibraryDidInstall(event: { item: LibraryPackage }): void; notifyLibraryDidUninstall(event: { item: LibraryPackage }): void; + + // Boards discovery notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void; notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void; } diff --git a/arduino-ide-extension/src/node/board-discovery.ts b/arduino-ide-extension/src/node/board-discovery.ts index 84ac101b6..bce8a36d0 100644 --- a/arduino-ide-extension/src/node/board-discovery.ts +++ b/arduino-ide-extension/src/node/board-discovery.ts @@ -6,7 +6,7 @@ import { deepClone } from '@theia/core/lib/common/objects'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { BackendApplicationContribution } from '@theia/core/lib/node'; import { inject, injectable, named } from '@theia/core/shared/inversify'; -import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; +import { Disposable } from '@theia/core/lib/common/disposable'; import { v4 } from 'uuid'; import { Unknown } from '../common/nls'; import { @@ -78,14 +78,6 @@ export class BoardDiscovery onStart(): void { this.start(); - this.onClientDidRefresh(() => this.restart()); - } - - private async restart(): Promise { - this.logger.info('restarting before stop'); - await this.stop(); - this.logger.info('restarting after stop'); - return this.start(); } onStop(): void { diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.d.ts index a15ce4719..ff43f4ea0 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.d.ts @@ -220,6 +220,9 @@ export class UpdateIndexRequest extends jspb.Message { getInstance(): cc_arduino_cli_commands_v1_common_pb.Instance | undefined; setInstance(value?: cc_arduino_cli_commands_v1_common_pb.Instance): UpdateIndexRequest; + getIgnoreCustomPackageIndexes(): boolean; + setIgnoreCustomPackageIndexes(value: boolean): UpdateIndexRequest; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): UpdateIndexRequest.AsObject; @@ -234,6 +237,7 @@ export class UpdateIndexRequest extends jspb.Message { export namespace UpdateIndexRequest { export type AsObject = { instance?: cc_arduino_cli_commands_v1_common_pb.Instance.AsObject, + ignoreCustomPackageIndexes: boolean, } } diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.js b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.js index 820634067..350a37d8e 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.js +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_pb.js @@ -1811,7 +1811,8 @@ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.prototype.toObject = functio */ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.toObject = function(includeInstance, msg) { var f, obj = { - instance: (f = msg.getInstance()) && cc_arduino_cli_commands_v1_common_pb.Instance.toObject(includeInstance, f) + instance: (f = msg.getInstance()) && cc_arduino_cli_commands_v1_common_pb.Instance.toObject(includeInstance, f), + ignoreCustomPackageIndexes: jspb.Message.getBooleanFieldWithDefault(msg, 2, false) }; if (includeInstance) { @@ -1853,6 +1854,10 @@ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.deserializeBinaryFromReader reader.readMessage(value,cc_arduino_cli_commands_v1_common_pb.Instance.deserializeBinaryFromReader); msg.setInstance(value); break; + case 2: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setIgnoreCustomPackageIndexes(value); + break; default: reader.skipField(); break; @@ -1890,6 +1895,13 @@ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.serializeBinaryToWriter = fu cc_arduino_cli_commands_v1_common_pb.Instance.serializeBinaryToWriter ); } + f = message.getIgnoreCustomPackageIndexes(); + if (f) { + writer.writeBool( + 2, + f + ); + } }; @@ -1930,6 +1942,24 @@ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.prototype.hasInstance = func }; +/** + * optional bool ignore_custom_package_indexes = 2; + * @return {boolean} + */ +proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.prototype.getIgnoreCustomPackageIndexes = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 2, false)); +}; + + +/** + * @param {boolean} value + * @return {!proto.cc.arduino.cli.commands.v1.UpdateIndexRequest} returns this + */ +proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.prototype.setIgnoreCustomPackageIndexes = function(value) { + return jspb.Message.setProto3BooleanField(this, 2, value); +}; + + diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/common_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/common_pb.d.ts index f79eb14c0..de19321dc 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/common_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/common_pb.d.ts @@ -28,21 +28,26 @@ export namespace Instance { } export class DownloadProgress extends jspb.Message { - getUrl(): string; - setUrl(value: string): DownloadProgress; - getFile(): string; - setFile(value: string): DownloadProgress; + hasStart(): boolean; + clearStart(): void; + getStart(): DownloadProgressStart | undefined; + setStart(value?: DownloadProgressStart): DownloadProgress; - getTotalSize(): number; - setTotalSize(value: number): DownloadProgress; - getDownloaded(): number; - setDownloaded(value: number): DownloadProgress; + hasUpdate(): boolean; + clearUpdate(): void; + getUpdate(): DownloadProgressUpdate | undefined; + setUpdate(value?: DownloadProgressUpdate): DownloadProgress; - getCompleted(): boolean; - setCompleted(value: boolean): DownloadProgress; + hasEnd(): boolean; + clearEnd(): void; + getEnd(): DownloadProgressEnd | undefined; + setEnd(value?: DownloadProgressEnd): DownloadProgress; + + + getMessageCase(): DownloadProgress.MessageCase; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): DownloadProgress.AsObject; @@ -55,12 +60,97 @@ export class DownloadProgress extends jspb.Message { } export namespace DownloadProgress { + export type AsObject = { + start?: DownloadProgressStart.AsObject, + update?: DownloadProgressUpdate.AsObject, + end?: DownloadProgressEnd.AsObject, + } + + export enum MessageCase { + MESSAGE_NOT_SET = 0, + + START = 1, + + UPDATE = 2, + + END = 3, + + } + +} + +export class DownloadProgressStart extends jspb.Message { + getUrl(): string; + setUrl(value: string): DownloadProgressStart; + + getLabel(): string; + setLabel(value: string): DownloadProgressStart; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): DownloadProgressStart.AsObject; + static toObject(includeInstance: boolean, msg: DownloadProgressStart): DownloadProgressStart.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: DownloadProgressStart, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): DownloadProgressStart; + static deserializeBinaryFromReader(message: DownloadProgressStart, reader: jspb.BinaryReader): DownloadProgressStart; +} + +export namespace DownloadProgressStart { export type AsObject = { url: string, - file: string, - totalSize: number, + label: string, + } +} + +export class DownloadProgressUpdate extends jspb.Message { + getDownloaded(): number; + setDownloaded(value: number): DownloadProgressUpdate; + + getTotalSize(): number; + setTotalSize(value: number): DownloadProgressUpdate; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): DownloadProgressUpdate.AsObject; + static toObject(includeInstance: boolean, msg: DownloadProgressUpdate): DownloadProgressUpdate.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: DownloadProgressUpdate, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): DownloadProgressUpdate; + static deserializeBinaryFromReader(message: DownloadProgressUpdate, reader: jspb.BinaryReader): DownloadProgressUpdate; +} + +export namespace DownloadProgressUpdate { + export type AsObject = { downloaded: number, - completed: boolean, + totalSize: number, + } +} + +export class DownloadProgressEnd extends jspb.Message { + getSuccess(): boolean; + setSuccess(value: boolean): DownloadProgressEnd; + + getMessage(): string; + setMessage(value: string): DownloadProgressEnd; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): DownloadProgressEnd.AsObject; + static toObject(includeInstance: boolean, msg: DownloadProgressEnd): DownloadProgressEnd.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: DownloadProgressEnd, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): DownloadProgressEnd; + static deserializeBinaryFromReader(message: DownloadProgressEnd, reader: jspb.BinaryReader): DownloadProgressEnd; +} + +export namespace DownloadProgressEnd { + export type AsObject = { + success: boolean, + message: string, } } diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/common_pb.js b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/common_pb.js index 4ffbe5d22..7b7a4bcd3 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/common_pb.js +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/common_pb.js @@ -17,6 +17,10 @@ var global = Function('return this')(); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.Board', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgress', null, global); +goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgress.MessageCase', null, global); +goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgressEnd', null, global); +goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgressStart', null, global); +goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.InstalledPlatformReference', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.Instance', null, global); goog.exportSymbol('proto.cc.arduino.cli.commands.v1.Platform', null, global); @@ -55,7 +59,7 @@ if (goog.DEBUG && !COMPILED) { * @constructor */ proto.cc.arduino.cli.commands.v1.DownloadProgress = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, null, null); + jspb.Message.initialize(this, opt_data, 0, -1, null, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_); }; goog.inherits(proto.cc.arduino.cli.commands.v1.DownloadProgress, jspb.Message); if (goog.DEBUG && !COMPILED) { @@ -65,6 +69,69 @@ if (goog.DEBUG && !COMPILED) { */ proto.cc.arduino.cli.commands.v1.DownloadProgress.displayName = 'proto.cc.arduino.cli.commands.v1.DownloadProgress'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressStart = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.cc.arduino.cli.commands.v1.DownloadProgressStart, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.cc.arduino.cli.commands.v1.DownloadProgressStart.displayName = 'proto.cc.arduino.cli.commands.v1.DownloadProgressStart'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.displayName = 'proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressEnd = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.cc.arduino.cli.commands.v1.DownloadProgressEnd, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.displayName = 'proto.cc.arduino.cli.commands.v1.DownloadProgressEnd'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -322,6 +389,33 @@ proto.cc.arduino.cli.commands.v1.Instance.prototype.setId = function(value) { +/** + * Oneof group definitions for this message. Each group defines the field + * numbers belonging to that group. When of these fields' value is set, all + * other fields in the group are cleared. During deserialization, if multiple + * fields are encountered for a group, only the last value seen will be kept. + * @private {!Array>} + * @const + */ +proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_ = [[1,2,3]]; + +/** + * @enum {number} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgress.MessageCase = { + MESSAGE_NOT_SET: 0, + START: 1, + UPDATE: 2, + END: 3 +}; + +/** + * @return {proto.cc.arduino.cli.commands.v1.DownloadProgress.MessageCase} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getMessageCase = function() { + return /** @type {proto.cc.arduino.cli.commands.v1.DownloadProgress.MessageCase} */(jspb.Message.computeOneofCase(this, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_[0])); +}; + if (jspb.Message.GENERATE_TO_OBJECT) { @@ -353,11 +447,9 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.toObject = function( */ proto.cc.arduino.cli.commands.v1.DownloadProgress.toObject = function(includeInstance, msg) { var f, obj = { - url: jspb.Message.getFieldWithDefault(msg, 1, ""), - file: jspb.Message.getFieldWithDefault(msg, 2, ""), - totalSize: jspb.Message.getFieldWithDefault(msg, 3, 0), - downloaded: jspb.Message.getFieldWithDefault(msg, 4, 0), - completed: jspb.Message.getBooleanFieldWithDefault(msg, 5, false) + start: (f = msg.getStart()) && proto.cc.arduino.cli.commands.v1.DownloadProgressStart.toObject(includeInstance, f), + update: (f = msg.getUpdate()) && proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.toObject(includeInstance, f), + end: (f = msg.getEnd()) && proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.toObject(includeInstance, f) }; if (includeInstance) { @@ -395,24 +487,19 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.deserializeBinaryFromReader = var field = reader.getFieldNumber(); switch (field) { case 1: - var value = /** @type {string} */ (reader.readString()); - msg.setUrl(value); + var value = new proto.cc.arduino.cli.commands.v1.DownloadProgressStart; + reader.readMessage(value,proto.cc.arduino.cli.commands.v1.DownloadProgressStart.deserializeBinaryFromReader); + msg.setStart(value); break; case 2: - var value = /** @type {string} */ (reader.readString()); - msg.setFile(value); + var value = new proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate; + reader.readMessage(value,proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.deserializeBinaryFromReader); + msg.setUpdate(value); break; case 3: - var value = /** @type {number} */ (reader.readInt64()); - msg.setTotalSize(value); - break; - case 4: - var value = /** @type {number} */ (reader.readInt64()); - msg.setDownloaded(value); - break; - case 5: - var value = /** @type {boolean} */ (reader.readBool()); - msg.setCompleted(value); + var value = new proto.cc.arduino.cli.commands.v1.DownloadProgressEnd; + reader.readMessage(value,proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.deserializeBinaryFromReader); + msg.setEnd(value); break; default: reader.skipField(); @@ -443,38 +530,262 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.serializeBinary = fu */ proto.cc.arduino.cli.commands.v1.DownloadProgress.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getUrl(); - if (f.length > 0) { - writer.writeString( + f = message.getStart(); + if (f != null) { + writer.writeMessage( 1, - f + f, + proto.cc.arduino.cli.commands.v1.DownloadProgressStart.serializeBinaryToWriter ); } - f = message.getFile(); - if (f.length > 0) { - writer.writeString( + f = message.getUpdate(); + if (f != null) { + writer.writeMessage( 2, - f + f, + proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.serializeBinaryToWriter ); } - f = message.getTotalSize(); - if (f !== 0) { - writer.writeInt64( + f = message.getEnd(); + if (f != null) { + writer.writeMessage( 3, - f + f, + proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.serializeBinaryToWriter ); } - f = message.getDownloaded(); - if (f !== 0) { - writer.writeInt64( - 4, +}; + + +/** + * optional DownloadProgressStart start = 1; + * @return {?proto.cc.arduino.cli.commands.v1.DownloadProgressStart} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getStart = function() { + return /** @type{?proto.cc.arduino.cli.commands.v1.DownloadProgressStart} */ ( + jspb.Message.getWrapperField(this, proto.cc.arduino.cli.commands.v1.DownloadProgressStart, 1)); +}; + + +/** + * @param {?proto.cc.arduino.cli.commands.v1.DownloadProgressStart|undefined} value + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this +*/ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setStart = function(value) { + return jspb.Message.setOneofWrapperField(this, 1, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this + */ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.clearStart = function() { + return this.setStart(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.hasStart = function() { + return jspb.Message.getField(this, 1) != null; +}; + + +/** + * optional DownloadProgressUpdate update = 2; + * @return {?proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getUpdate = function() { + return /** @type{?proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} */ ( + jspb.Message.getWrapperField(this, proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate, 2)); +}; + + +/** + * @param {?proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate|undefined} value + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this +*/ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setUpdate = function(value) { + return jspb.Message.setOneofWrapperField(this, 2, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this + */ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.clearUpdate = function() { + return this.setUpdate(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.hasUpdate = function() { + return jspb.Message.getField(this, 2) != null; +}; + + +/** + * optional DownloadProgressEnd end = 3; + * @return {?proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getEnd = function() { + return /** @type{?proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} */ ( + jspb.Message.getWrapperField(this, proto.cc.arduino.cli.commands.v1.DownloadProgressEnd, 3)); +}; + + +/** + * @param {?proto.cc.arduino.cli.commands.v1.DownloadProgressEnd|undefined} value + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this +*/ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setEnd = function(value) { + return jspb.Message.setOneofWrapperField(this, 3, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this + */ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.clearEnd = function() { + return this.setEnd(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.hasEnd = function() { + return jspb.Message.getField(this, 3) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.toObject = function(opt_includeInstance) { + return proto.cc.arduino.cli.commands.v1.DownloadProgressStart.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressStart.toObject = function(includeInstance, msg) { + var f, obj = { + url: jspb.Message.getFieldWithDefault(msg, 1, ""), + label: jspb.Message.getFieldWithDefault(msg, 2, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressStart.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.cc.arduino.cli.commands.v1.DownloadProgressStart; + return proto.cc.arduino.cli.commands.v1.DownloadProgressStart.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressStart.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setUrl(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setLabel(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.cc.arduino.cli.commands.v1.DownloadProgressStart.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressStart.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getUrl(); + if (f.length > 0) { + writer.writeString( + 1, f ); } - f = message.getCompleted(); - if (f) { - writer.writeBool( - 5, + f = message.getLabel(); + if (f.length > 0) { + writer.writeString( + 2, f ); } @@ -485,89 +796,355 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.serializeBinaryToWriter = func * optional string url = 1; * @return {string} */ -proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getUrl = function() { +proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.getUrl = function() { return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); }; /** * @param {string} value - * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} returns this */ -proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setUrl = function(value) { +proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.setUrl = function(value) { return jspb.Message.setProto3StringField(this, 1, value); }; /** - * optional string file = 2; + * optional string label = 2; * @return {string} */ -proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getFile = function() { +proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.getLabel = function() { return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); }; /** * @param {string} value - * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} returns this */ -proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setFile = function(value) { +proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.setLabel = function(value) { return jspb.Message.setProto3StringField(this, 2, value); }; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.toObject = function(opt_includeInstance) { + return proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.toObject = function(includeInstance, msg) { + var f, obj = { + downloaded: jspb.Message.getFieldWithDefault(msg, 1, 0), + totalSize: jspb.Message.getFieldWithDefault(msg, 2, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + /** - * optional int64 total_size = 3; + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate; + return proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readInt64()); + msg.setDownloaded(value); + break; + case 2: + var value = /** @type {number} */ (reader.readInt64()); + msg.setTotalSize(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getDownloaded(); + if (f !== 0) { + writer.writeInt64( + 1, + f + ); + } + f = message.getTotalSize(); + if (f !== 0) { + writer.writeInt64( + 2, + f + ); + } +}; + + +/** + * optional int64 downloaded = 1; * @return {number} */ -proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getTotalSize = function() { - return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.getDownloaded = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); }; /** * @param {number} value - * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} returns this */ -proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setTotalSize = function(value) { - return jspb.Message.setProto3IntField(this, 3, value); +proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.setDownloaded = function(value) { + return jspb.Message.setProto3IntField(this, 1, value); }; /** - * optional int64 downloaded = 4; + * optional int64 total_size = 2; * @return {number} */ -proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getDownloaded = function() { - return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 4, 0)); +proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.getTotalSize = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); }; /** * @param {number} value - * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} returns this + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.setTotalSize = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.toObject = function(opt_includeInstance) { + return proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.toObject = function(includeInstance, msg) { + var f, obj = { + success: jspb.Message.getBooleanFieldWithDefault(msg, 1, false), + message: jspb.Message.getFieldWithDefault(msg, 2, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.cc.arduino.cli.commands.v1.DownloadProgressEnd; + return proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setSuccess(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setMessage(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} */ -proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setDownloaded = function(value) { - return jspb.Message.setProto3IntField(this, 4, value); +proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); }; /** - * optional bool completed = 5; + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getSuccess(); + if (f) { + writer.writeBool( + 1, + f + ); + } + f = message.getMessage(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } +}; + + +/** + * optional bool success = 1; * @return {boolean} */ -proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getCompleted = function() { - return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 5, false)); +proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.getSuccess = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 1, false)); }; /** * @param {boolean} value - * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} returns this */ -proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setCompleted = function(value) { - return jspb.Message.setProto3BooleanField(this, 5, value); +proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.setSuccess = function(value) { + return jspb.Message.setProto3BooleanField(this, 1, value); +}; + + +/** + * optional string message = 2; + * @return {string} + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.getMessage = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * @param {string} value + * @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} returns this + */ +proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.setMessage = function(value) { + return jspb.Message.setProto3StringField(this, 2, value); }; diff --git a/arduino-ide-extension/src/node/core-client-provider.ts b/arduino-ide-extension/src/node/core-client-provider.ts index 0bfd9766b..1686be1be 100644 --- a/arduino-ide-extension/src/node/core-client-provider.ts +++ b/arduino-ide-extension/src/node/core-client-provider.ts @@ -5,7 +5,7 @@ import { injectable, postConstruct, } from '@theia/core/shared/inversify'; -import { Emitter, Event } from '@theia/core/lib/common/event'; +import { Emitter } from '@theia/core/lib/common/event'; import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb'; import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; import { @@ -19,8 +19,15 @@ import { UpdateLibrariesIndexResponse, } from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb'; import * as commandsGrpcPb from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb'; -import { NotificationServiceServer } from '../common/protocol'; -import { Deferred, retry } from '@theia/core/lib/common/promise-util'; +import { + IndexType, + IndexUpdateDidCompleteParams, + IndexUpdateSummary, + IndexUpdateDidFailParams, + IndexUpdateWillStartParams, + NotificationServiceServer, +} from '../common/protocol'; +import { Deferred } from '@theia/core/lib/common/promise-util'; import { Status as RpcStatus, Status, @@ -32,6 +39,7 @@ import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; import { IndexesUpdateProgressHandler, ExecuteWithProgress, + DownloadResult, } from './grpc-progressible'; import type { DefaultCliConfig } from './cli-config'; import { ServiceError } from './service-error'; @@ -45,16 +53,19 @@ export class CoreClientProvider { @inject(NotificationServiceServer) private readonly notificationService: NotificationServiceServer; - private ready = new Deferred(); - private pending: Deferred | undefined; - private _client: CoreClientProvider.Client | undefined; - private readonly toDisposeBeforeCreate = new DisposableCollection(); + /** + * See `CoreService#indexUpdateSummaryBeforeInit`. + */ + private readonly beforeInitSummary = {} as IndexUpdateSummary; + private readonly toDisposeOnCloseClient = new DisposableCollection(); private readonly toDisposeAfterDidCreate = new DisposableCollection(); private readonly onClientReadyEmitter = new Emitter(); private readonly onClientReady = this.onClientReadyEmitter.event; - private readonly onClientDidRefreshEmitter = - new Emitter(); + + private ready = new Deferred(); + private pending: Deferred | undefined; + private _client: CoreClientProvider.Client | undefined; @postConstruct() protected init(): void { @@ -65,7 +76,9 @@ export class CoreClientProvider { }); this.daemon.onDaemonStarted((port) => this.create(port)); this.daemon.onDaemonStopped(() => this.closeClient()); - this.configService.onConfigChange(() => this.refreshIndexes()); + this.configService.onConfigChange( + () => this.client.then((client) => this.updateIndex(client, ['platform'])) // Assuming 3rd party URL changes. No library index update is required. + ); } get tryGetClient(): CoreClientProvider.Client | undefined { @@ -80,7 +93,7 @@ export class CoreClientProvider { if (!this.pending) { this.pending = new Deferred(); this.toDisposeAfterDidCreate.pushAll([ - Disposable.create(() => (this.pending = undefined)), + Disposable.create(() => (this.pending = undefined)), // TODO: reject all pending requests before unsetting the ref? this.onClientReady((client) => { this.pending?.resolve(client); this.toDisposeAfterDidCreate.dispose(); @@ -90,10 +103,6 @@ export class CoreClientProvider { return this.pending.promise; } - get onClientDidRefresh(): Event { - return this.onClientDidRefreshEmitter.event; - } - async refresh(): Promise { const client = await this.client; await this.initInstance(client); @@ -106,7 +115,7 @@ export class CoreClientProvider { this.closeClient(); const address = this.address(port); const client = await this.createClient(address); - this.toDisposeBeforeCreate.pushAll([ + this.toDisposeOnCloseClient.pushAll([ Disposable.create(() => client.client.close()), Disposable.create(() => { this.ready.reject( @@ -118,7 +127,6 @@ export class CoreClientProvider { }), ]); await this.initInstanceWithFallback(client); - setTimeout(async () => this.refreshIndexes(), 10_000); // Update the indexes asynchronously return this.useClient(client); } @@ -141,12 +149,17 @@ export class CoreClientProvider { try { await this.initInstance(client); } catch (err) { - if (err instanceof IndexUpdateRequiredBeforeInitError) { + if (err instanceof MustUpdateIndexesBeforeInitError) { console.error( 'The primary packages indexes are missing. Running indexes update before initializing the core gRPC client', err.message ); - await this.updateIndexes(client); // TODO: this should run without the 3rd party URLs + await this.updateIndex(client, Array.from(err.indexTypesToUpdate)); + const updatedAt = new Date().toISOString(); + // Clients will ask for it after they connect. + err.indexTypesToUpdate.forEach( + (type) => (this.beforeInitSummary[type] = updatedAt) + ); await this.initInstance(client); console.info( `Downloaded the primary package indexes, and successfully initialized the core gRPC client.` @@ -170,7 +183,7 @@ export class CoreClientProvider { } private closeClient(): void { - return this.toDisposeBeforeCreate.dispose(); + return this.toDisposeOnCloseClient.dispose(); } private async createClient( @@ -253,45 +266,66 @@ export class CoreClientProvider { } /** - * Updates all indexes and runs an init to [reload the indexes](https://github.com/arduino/arduino-cli/pull/1274#issue-866154638). + * `update3rdPartyPlatforms` has not effect if `types` is `['library']`. */ - private async refreshIndexes(): Promise { - const client = this._client; - if (client) { - const progressHandler = this.createProgressHandler(); - try { - await this.updateIndexes(client, progressHandler); + async updateIndex( + client: CoreClientProvider.Client, + types: IndexType[] + ): Promise { + let error: unknown | undefined = undefined; + const progressHandler = this.createProgressHandler(types); + try { + const updates: Promise[] = []; + if (types.includes('platform')) { + updates.push(this.updatePlatformIndex(client, progressHandler)); + } + if (types.includes('library')) { + updates.push(this.updateLibraryIndex(client, progressHandler)); + } + await Promise.all(updates); + } catch (err) { + // This is suboptimal but the core client must be re-initialized even if the index update has failed and the request was rejected. + error = err; + } finally { + // IDE2 reloads the index only and if only at least one download success is available. + if ( + progressHandler.results.some( + (result) => !DownloadResult.isError(result) + ) + ) { await this.initInstance(client); // notify clients about the index update only after the client has been "re-initialized" and the new content is available. progressHandler.reportEnd(); - this.onClientDidRefreshEmitter.fire(client); - } catch (err) { - console.error('Failed to update indexes', err); - progressHandler.reportError( - ServiceError.is(err) ? err.details : String(err) - ); + } + if (error) { + console.error(`Failed to update ${types.join(', ')} indexes.`, error); + const downloadErrors = progressHandler.results + .filter(DownloadResult.isError) + .map(({ url, message }) => `${message}: ${url}`) + .join(' '); + const message = ServiceError.is(error) + ? `${error.details}${downloadErrors ? ` ${downloadErrors}` : ''}` + : String(error); + // IDE2 keeps only the most recent error message. Previous errors might have been fixed with the fallback initialization. + this.beforeInitSummary.message = message; + // Toast the error message, so tha the user has chance to fix it if it was a client error (HTTP 4xx). + progressHandler.reportError(message); } } } - private async updateIndexes( - client: CoreClientProvider.Client, - progressHandler?: IndexesUpdateProgressHandler - ): Promise { - await Promise.all([ - this.updateIndex(client, progressHandler), - this.updateLibraryIndex(client, progressHandler), - ]); + get indexUpdateSummaryBeforeInit(): IndexUpdateSummary { + return { ...this.beforeInitSummary }; } - private async updateIndex( + private async updatePlatformIndex( client: CoreClientProvider.Client, progressHandler?: IndexesUpdateProgressHandler ): Promise { return this.doUpdateIndex( () => client.client.updateIndex( - new UpdateIndexRequest().setInstance(client.instance) + new UpdateIndexRequest().setInstance(client.instance) // Always updates both the primary and the 3rd party package indexes. ), progressHandler, 'platform-index' @@ -323,50 +357,45 @@ export class CoreClientProvider { task?: string ): Promise { const progressId = progressHandler?.progressId; - return retry( - () => - new Promise((resolve, reject) => { - responseProvider() - .on( - 'data', - ExecuteWithProgress.createDataCallback({ - responseService: { - appendToOutput: ({ chunk: message }) => { - console.log( - `core-client-provider${task ? ` [${task}]` : ''}`, - message - ); - progressHandler?.reportProgress(message); - }, - }, - progressId, - }) - ) - .on('error', reject) - .on('end', resolve); - }), - 50, - 3 - ); + return new Promise((resolve, reject) => { + responseProvider() + .on( + 'data', + ExecuteWithProgress.createDataCallback({ + responseService: { + appendToOutput: ({ chunk: message }) => { + console.log( + `core-client-provider${task ? ` [${task}]` : ''}`, + message + ); + progressHandler?.reportProgress(message); + }, + reportResult: (result) => progressHandler?.reportResult(result), + }, + progressId, + }) + ) + .on('error', reject) + .on('end', resolve); + }); } - private createProgressHandler(): IndexesUpdateProgressHandler { + private createProgressHandler( + types: IndexType[] + ): IndexesUpdateProgressHandler { const additionalUrlsCount = this.configService.cliConfiguration?.board_manager?.additional_urls ?.length ?? 0; - return new IndexesUpdateProgressHandler( - additionalUrlsCount, - (progressMessage) => + return new IndexesUpdateProgressHandler(types, additionalUrlsCount, { + onProgress: (progressMessage) => this.notificationService.notifyIndexUpdateDidProgress(progressMessage), - ({ progressId, message }) => - this.notificationService.notifyIndexUpdateDidFail({ - progressId, - message, - }), - (progressId) => - this.notificationService.notifyIndexWillUpdate(progressId), - (progressId) => this.notificationService.notifyIndexDidUpdate(progressId) - ); + onError: (params: IndexUpdateDidFailParams) => + this.notificationService.notifyIndexUpdateDidFail(params), + onStart: (params: IndexUpdateWillStartParams) => + this.notificationService.notifyIndexUpdateWillStart(params), + onComplete: (params: IndexUpdateDidCompleteParams) => + this.notificationService.notifyIndexUpdateDidComplete(params), + }); } private address(port: string): string { @@ -410,6 +439,7 @@ export namespace CoreClientProvider { export abstract class CoreClientAware { @inject(CoreClientProvider) private readonly coreClientProvider: CoreClientProvider; + /** * Returns with a promise that resolves when the core client is initialized and ready. */ @@ -417,8 +447,17 @@ export abstract class CoreClientAware { return this.coreClientProvider.client; } - protected get onClientDidRefresh(): Event { - return this.coreClientProvider.onClientDidRefresh; + /** + * Updates the index of the given `type` and returns with a promise which resolves when the core gPRC client has been reinitialized. + */ + async updateIndex({ types }: { types: IndexType[] }): Promise { + const client = await this.coreClient; + return this.coreClientProvider.updateIndex(client, types); + } + + async indexUpdateSummaryBeforeInit(): Promise { + await this.coreClient; + return this.coreClientProvider.indexUpdateSummaryBeforeInit; } refresh(): Promise { @@ -426,15 +465,20 @@ export abstract class CoreClientAware { } } -class IndexUpdateRequiredBeforeInitError extends Error { - constructor(causes: RpcStatus.AsObject[]) { +class MustUpdateIndexesBeforeInitError extends Error { + readonly indexTypesToUpdate: Set; + constructor(causes: [RpcStatus.AsObject, IndexType][]) { super(`The index of the cores and libraries must be updated before initializing the core gRPC client. The following problems were detected during the gRPC client initialization: ${causes - .map(({ code, message }) => ` - code: ${code}, message: ${message}`) + .map( + ([{ code, message }, type]) => + `[${type}-index] - code: ${code}, message: ${message}` + ) .join('\n')} `); - Object.setPrototypeOf(this, IndexUpdateRequiredBeforeInitError.prototype); + Object.setPrototypeOf(this, MustUpdateIndexesBeforeInitError.prototype); + this.indexTypesToUpdate = new Set(causes.map(([, type]) => type)); if (!causes.length) { throw new Error(`expected non-empty 'causes'`); } @@ -444,41 +488,66 @@ ${causes function isIndexUpdateRequiredBeforeInit( status: RpcStatus[], cliConfig: DefaultCliConfig -): IndexUpdateRequiredBeforeInitError | undefined { - const causes = status - .filter((s) => - IndexUpdateRequiredBeforeInit.map((predicate) => - predicate(s, cliConfig) - ).some(Boolean) - ) - .map((s) => RpcStatus.toObject(false, s)); +): MustUpdateIndexesBeforeInitError | undefined { + const causes = status.reduce((acc, curr) => { + for (const [predicate, type] of IndexUpdateRequiredPredicates) { + if (predicate(curr, cliConfig)) { + acc.push([curr.toObject(false), type]); + return acc; + } + } + return acc; + }, [] as [RpcStatus.AsObject, IndexType][]); return causes.length - ? new IndexUpdateRequiredBeforeInitError(causes) + ? new MustUpdateIndexesBeforeInitError(causes) : undefined; } -const IndexUpdateRequiredBeforeInit = [ - isPackageIndexMissingStatus, - isDiscoveryNotFoundStatus, +interface Predicate { + ( + status: RpcStatus, + { + directories: { data }, + }: DefaultCliConfig + ): boolean; +} +const IndexUpdateRequiredPredicates: [Predicate, IndexType][] = [ + [isPrimaryPackageIndexMissingStatus, 'platform'], + [isDiscoveryNotFoundStatus, 'platform'], + [isLibraryIndexMissingStatus, 'library'], ]; -function isPackageIndexMissingStatus( +// Loading index file: loading json index file /path/to/package_index.json: open /path/to/package_index.json: no such file or directory +function isPrimaryPackageIndexMissingStatus( status: RpcStatus, { directories: { data } }: DefaultCliConfig ): boolean { const predicate = ({ message }: RpcStatus.AsObject) => message.includes('loading json index file') && - (message.includes(join(data, 'package_index.json')) || - message.includes(join(data, 'library_index.json'))); + message.includes(join(data, 'package_index.json')); // https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247 return evaluate(status, predicate); } +// Error loading hardware platform: discovery $TOOL_NAME not found function isDiscoveryNotFoundStatus(status: RpcStatus): boolean { const predicate = ({ message }: RpcStatus.AsObject) => message.includes('discovery') && - (message.includes('not found') || message.includes('not installed')); + (message.includes('not found') || + message.includes('loading hardware platform')); // https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L740 // https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L744 return evaluate(status, predicate); } +// Loading index file: reading library_index.json: open /path/to/library_index.json: no such file or directory +function isLibraryIndexMissingStatus( + status: RpcStatus, + { directories: { data } }: DefaultCliConfig +): boolean { + const predicate = ({ message }: RpcStatus.AsObject) => + message.includes('index file') && + message.includes('reading') && + message.includes(join(data, 'library_index.json')); + // https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247 + return evaluate(status, predicate); +} function evaluate( subject: RpcStatus, predicate: (error: RpcStatus.AsObject) => boolean diff --git a/arduino-ide-extension/src/node/grpc-progressible.ts b/arduino-ide-extension/src/node/grpc-progressible.ts index c9b93183d..edd04cd85 100644 --- a/arduino-ide-extension/src/node/grpc-progressible.ts +++ b/arduino-ide-extension/src/node/grpc-progressible.ts @@ -1,4 +1,11 @@ import { v4 } from 'uuid'; +import { + IndexType, + IndexUpdateDidCompleteParams, + IndexUpdateDidFailParams, + IndexUpdateSummary, + IndexUpdateWillStartParams, +} from '../common/protocol'; import { ProgressMessage, ResponseService, @@ -11,6 +18,9 @@ import { import { DownloadProgress, TaskProgress, + DownloadProgressStart, + DownloadProgressUpdate, + DownloadProgressEnd, } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; import { CompileResponse } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb'; import { @@ -81,7 +91,9 @@ namespace IndexProgressResponse { ); } export function workUnit(response: IndexProgressResponse): UnitOfWork { - return { download: response.getDownloadProgress() }; + return { + download: response.getDownloadProgress(), + }; } } /** @@ -151,7 +163,9 @@ export namespace ExecuteWithProgress { * _unknown_ progress if falsy. */ readonly progressId?: string; - readonly responseService: Partial; + readonly responseService: Partial< + ResponseService & { reportResult: (result: DownloadResult) => void } + >; } export function createDataCallback({ @@ -159,19 +173,21 @@ export namespace ExecuteWithProgress { progressId, }: ExecuteWithProgress.Options): (response: R) => void { const uuid = v4(); - let localFile = ''; - let localTotalSize = Number.NaN; + let message = ''; + let url = ''; return (response: R) => { if (DEBUG) { const json = toJson(response); if (json) { - console.log(`Progress response [${uuid}]: ${json}`); + console.debug(`[gRPC progress] Progress response [${uuid}]: ${json}`); } } const unitOfWork = resolve(response); const { task, download } = unitOfWork; if (!download && !task) { - // report a fake unknown progress. + // Report a fake unknown progress if progress ID is available. + // When a progress ID is available, a connected client is setting the progress ID. + // Hence, it's listening to progress updates. if (unitOfWork === UnitOfWork.Unknown && progressId) { if (progressId) { responseService.reportProgress?.({ @@ -187,7 +203,7 @@ export namespace ExecuteWithProgress { // 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." + `Implementation error. None of the following properties were available on the response: 'task', 'download'` ); } return; @@ -219,43 +235,32 @@ export namespace ExecuteWithProgress { } } } else if (download) { - if (download.getFile() && !localFile) { - localFile = download.getFile(); - } - if (download.getTotalSize() > 0 && Number.isNaN(localTotalSize)) { - localTotalSize = download.getTotalSize(); - } - - // This happens only once per file download. - if (download.getTotalSize() && localFile) { - responseService.appendToOutput?.({ chunk: `${localFile}\n` }); - } - - if (progressId && localFile) { - let work: ProgressMessage.Work | undefined = undefined; - if (download.getDownloaded() > 0 && !Number.isNaN(localTotalSize)) { - work = { - total: localTotalSize, - done: download.getDownloaded(), - }; - } - responseService.reportProgress?.({ - progressId, - message: `Downloading ${localFile}`, - work, - }); - } - if (download.getCompleted()) { - // Discard local state. - if (progressId && !Number.isNaN(localTotalSize)) { + const phase = phaseOf(download); + if (phase instanceof DownloadProgressStart) { + message = phase.getLabel(); + url = phase.getUrl(); + responseService.appendToOutput?.({ chunk: `${message}\n` }); + } else if (phase instanceof DownloadProgressUpdate) { + if (progressId && message) { responseService.reportProgress?.({ progressId, - message: '', - work: { done: Number.NaN, total: Number.NaN }, + message, + work: { + total: phase.getTotalSize(), + done: phase.getDownloaded(), + }, + }); + } + } else if (phase instanceof DownloadProgressEnd) { + if (url) { + responseService.reportResult?.({ + url, + message: phase.getMessage(), + success: phase.getSuccess(), }); } - localFile = ''; - localTotalSize = Number.NaN; + message = ''; + url = ''; } } }; @@ -274,31 +279,40 @@ export namespace ExecuteWithProgress { return {}; } function toJson(response: ProgressResponse): string | undefined { - let object: Record | undefined = undefined; - if (response instanceof LibraryInstallResponse) { - object = LibraryInstallResponse.toObject(false, response); - } else if (response instanceof LibraryUninstallResponse) { - object = LibraryUninstallResponse.toObject(false, response); - } else if (response instanceof ZipLibraryInstallResponse) { - object = ZipLibraryInstallResponse.toObject(false, response); - } else if (response instanceof PlatformInstallResponse) { - object = PlatformInstallResponse.toObject(false, response); - } else if (response instanceof PlatformUninstallResponse) { - object = PlatformUninstallResponse.toObject(false, response); - } else if (response instanceof UpdateIndexResponse) { - object = UpdateIndexResponse.toObject(false, response); - } else if (response instanceof UpdateLibrariesIndexResponse) { - object = UpdateLibrariesIndexResponse.toObject(false, response); - } else if (response instanceof UpdateCoreLibrariesIndexResponse) { - object = UpdateCoreLibrariesIndexResponse.toObject(false, response); - } else if (response instanceof CompileResponse) { - object = CompileResponse.toObject(false, response); + return JSON.stringify(response.toObject(false)); + } + function phaseOf( + download: DownloadProgress + ): DownloadProgressStart | DownloadProgressUpdate | DownloadProgressEnd { + let start: undefined | DownloadProgressStart = undefined; + let update: undefined | DownloadProgressUpdate = undefined; + let end: undefined | DownloadProgressEnd = undefined; + if (download.hasStart()) { + start = download.getStart(); + } else if (download.hasUpdate()) { + update = download.getUpdate(); + } else if (download.hasEnd()) { + end = download.getEnd(); + } else { + throw new Error( + `Download progress does not have a 'start', 'update', and 'end'. ${JSON.stringify( + download.toObject(false) + )}` + ); } - if (!object) { - console.warn('Unhandled gRPC response', response); - return undefined; + if (start) { + return start; + } else if (update) { + return update; + } else if (end) { + return end; + } else { + throw new Error( + `Download progress does not have a 'start', 'update', and 'end'. ${JSON.stringify( + download.toObject(false) + )}` + ); } - return JSON.stringify(object); } } @@ -306,33 +320,39 @@ export class IndexesUpdateProgressHandler { private done = 0; private readonly total: number; readonly progressId: string; + readonly results: DownloadResult[]; constructor( + private types: IndexType[], additionalUrlsCount: number, - private readonly onProgress: (progressMessage: ProgressMessage) => void, - private readonly onError?: ({ - progressId, - message, - }: { - progressId: string; - message: string; - }) => void, - private readonly onStart?: (progressId: string) => void, - private readonly onEnd?: (progressId: string) => void + private readonly options: { + onProgress: (progressMessage: ProgressMessage) => void; + onError?: (params: IndexUpdateDidFailParams) => void; + onStart?: (params: IndexUpdateWillStartParams) => void; + onComplete?: (params: IndexUpdateDidCompleteParams) => void; + } ) { this.progressId = v4(); - this.total = IndexesUpdateProgressHandler.total(additionalUrlsCount); + this.results = []; + this.total = IndexesUpdateProgressHandler.total(types, additionalUrlsCount); // Note: at this point, the IDE2 backend might not have any connected clients, so this notification is not delivered to anywhere - // Hence, clients must handle gracefully when no `willUpdate` is received before any `didProgress`. - this.onStart?.(this.progressId); + // Hence, clients must handle gracefully when no `willStart` event is received before any `didProgress`. + this.options.onStart?.({ progressId: this.progressId, types }); } reportEnd(): void { - this.onEnd?.(this.progressId); + const updatedAt = new Date().toISOString(); + this.options.onComplete?.({ + progressId: this.progressId, + summary: this.types.reduce((summary, type) => { + summary[type] = updatedAt; + return summary; + }, {} as IndexUpdateSummary), + }); } reportProgress(message: string): void { - this.onProgress({ + this.options.onProgress({ message, progressId: this.progressId, work: { total: this.total, done: ++this.done }, @@ -340,15 +360,44 @@ export class IndexesUpdateProgressHandler { } reportError(message: string): void { - this.onError?.({ progressId: this.progressId, message }); + this.options.onError?.({ + progressId: this.progressId, + message, + types: this.types, + }); } - private static total(additionalUrlsCount: number): number { - // +1 for the `package_index.tar.bz2` when updating the platform index. - const totalPlatformIndexCount = additionalUrlsCount + 1; - // The `library_index.json.gz` and `library_index.json.sig` when running the library index update. - const totalLibraryIndexCount = 2; + reportResult(result: DownloadResult): void { + this.results.push(result); + } + + private static total( + types: IndexType[], + additionalUrlsCount: number + ): number { + let total = 0; + if (types.includes('library')) { + // The `library_index.json.gz` and `library_index.json.sig` when running the library index update. + total += 2; + } + if (types.includes('platform')) { + // +1 for the `package_index.tar.bz2` when updating the platform index. + total += additionalUrlsCount + 1; + } // +1 for the `initInstance` call after the index update (`reportEnd`) - return totalPlatformIndexCount + totalLibraryIndexCount + 1; + return total + 1; + } +} + +export interface DownloadResult { + readonly url: string; + readonly success: boolean; + readonly message?: string; +} +export namespace DownloadResult { + export function isError( + arg: DownloadResult + ): arg is DownloadResult & { message: string } { + return !!arg.message && !arg.success; } } diff --git a/arduino-ide-extension/src/node/notification-service-server.ts b/arduino-ide-extension/src/node/notification-service-server.ts index 733edb336..3dc3280d7 100644 --- a/arduino-ide-extension/src/node/notification-service-server.ts +++ b/arduino-ide-extension/src/node/notification-service-server.ts @@ -8,6 +8,9 @@ import type { Config, Sketch, ProgressMessage, + IndexUpdateWillStartParams, + IndexUpdateDidCompleteParams, + IndexUpdateDidFailParams, } from '../common/protocol'; @injectable() @@ -16,8 +19,8 @@ export class NotificationServiceServerImpl { private readonly clients: NotificationServiceClient[] = []; - notifyIndexWillUpdate(progressId: string): void { - this.clients.forEach((client) => client.notifyIndexWillUpdate(progressId)); + notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void { + this.clients.forEach((client) => client.notifyIndexUpdateWillStart(params)); } notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void { @@ -26,22 +29,16 @@ export class NotificationServiceServerImpl ); } - notifyIndexDidUpdate(progressId: string): void { - this.clients.forEach((client) => client.notifyIndexDidUpdate(progressId)); - } - - notifyIndexUpdateDidFail({ - progressId, - message, - }: { - progressId: string; - message: string; - }): void { + notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void { this.clients.forEach((client) => - client.notifyIndexUpdateDidFail({ progressId, message }) + client.notifyIndexUpdateDidComplete(params) ); } + notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void { + this.clients.forEach((client) => client.notifyIndexUpdateDidFail(params)); + } + notifyDaemonDidStart(port: string): void { this.clients.forEach((client) => client.notifyDaemonDidStart(port)); } diff --git a/i18n/en.json b/i18n/en.json index c2dfd1e07..3480a2215 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -408,6 +408,11 @@ "dismissSurvey": "Don't show again", "surveyMessage": "Please help us improve by answering this super short survey. We value our community and would like to get to know our supporters a little better." }, + "updateIndexes": { + "updateIndexes": "Update Indexes", + "updateLibraryIndex": "Update Library Index", + "updatePackageIndex": "Update Package Index" + }, "upload": { "error": "{0} error: {1}" },