From beabb800bd055649c2d18d62a69b6d135c9446c0 Mon Sep 17 00:00:00 2001 From: Giacomo Cusinato <7659518+giacomocusinato@users.noreply.github.com> Date: Fri, 6 Sep 2024 01:49:53 +0200 Subject: [PATCH 1/2] feat: decode grpc status objects and map them to protocol types Status object thrown by grpc commands contains metadata that needs to be serialized in order to map it to custom errors generated through proto files https://github.com/grpc/grpc-web/issues/399 --- .../src/node/service-error.ts | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/arduino-ide-extension/src/node/service-error.ts b/arduino-ide-extension/src/node/service-error.ts index a56cf13ea..a42c05d8a 100644 --- a/arduino-ide-extension/src/node/service-error.ts +++ b/arduino-ide-extension/src/node/service-error.ts @@ -1,14 +1,54 @@ import { Metadata, StatusObject } from '@grpc/grpc-js'; +import { Status } from './cli-protocol/google/rpc/status_pb'; +import { stringToUint8Array } from '../common/utils'; +import { ProgrammerIsRequiredForUploadError } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; + +type ProtoError = typeof ProgrammerIsRequiredForUploadError; +const protoErrorsMap = new Map([ + [ + 'type.googleapis.com/cc.arduino.cli.commands.v1.ProgrammerIsRequiredForUploadError', + ProgrammerIsRequiredForUploadError, + ], + // handle other cli defined errors here +]); export type ServiceError = StatusObject & Error; export namespace ServiceError { export function isCancel(arg: unknown): arg is ServiceError & { code: 1 } { return is(arg) && arg.code === 1; // https://grpc.github.io/grpc/core/md_doc_statuscodes.html } + export function is(arg: unknown): arg is ServiceError { - return arg instanceof Error && isStatusObjet(arg); + return arg instanceof Error && isStatusObject(arg); } - function isStatusObjet(arg: unknown): arg is StatusObject { + + export function isInstanceOf(arg: unknown, type: unknown): boolean { + if (!isStatusObject(arg)) { + return false; + } + + const bin = arg.metadata.get('grpc-status-details-bin')[0]; + + const uint8Array = + typeof bin === 'string' + ? stringToUint8Array(bin) + : new Uint8Array(bin.buffer, bin.byteOffset, bin.byteLength); + + const errors = Status.deserializeBinary(uint8Array) + .getDetailsList() + .map((details) => { + const typeUrl = details.getTypeUrl(); + const ErrorType = protoErrorsMap.get(typeUrl); + return ErrorType?.deserializeBinary(details.getValue_asU8()); + }); + + return !!errors.find((error) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return error && error instanceof type; + }); + } + + function isStatusObject(arg: unknown): arg is StatusObject { if (typeof arg === 'object') { // eslint-disable-next-line @typescript-eslint/no-explicit-any const any = arg as any; From 3f325e005757d06c695a732d2c6314245e4ae542 Mon Sep 17 00:00:00 2001 From: Giacomo Cusinato <7659518+giacomocusinato@users.noreply.github.com> Date: Fri, 6 Sep 2024 01:53:34 +0200 Subject: [PATCH 2/2] feat: upload using programmer by default if board requires it --- .../browser/contributions/upload-sketch.ts | 31 +++++++++++++++++-- .../src/common/protocol/core-service.ts | 5 +++ .../src/node/core-service-impl.ts | 15 ++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index 9cee46abb..c21b86190 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -127,6 +127,7 @@ export class UploadSketch extends CoreServiceContribution { usingProgrammer, verifyOptions ); + if (!uploadOptions) { return; } @@ -137,11 +138,37 @@ export class UploadSketch extends CoreServiceContribution { const uploadResponse = await this.doWithProgress({ progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'), - task: (progressId, coreService, token) => - coreService.upload({ ...uploadOptions, progressId }, token), + task: async (progressId, coreService, token) => { + try { + return await coreService.upload( + { ...uploadOptions, progressId }, + token + ); + } catch (err) { + if (err.code === 4005) { + const uploadWithProgrammerOptions = await this.uploadOptions( + true, + verifyOptions + ); + if (uploadWithProgrammerOptions) { + return coreService.upload( + { ...uploadWithProgrammerOptions, progressId }, + token + ); + } + } else { + throw err; + } + } + }, keepOutput: true, cancelable: true, }); + + if (!uploadResponse) { + return; + } + // the port update is NOOP if nothing has changed this.boardsServiceProvider.updateConfig(uploadResponse.portAfterUpload); diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index f3a681d48..2b4a07652 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -71,6 +71,7 @@ export namespace CoreError { Upload: 4002, UploadUsingProgrammer: 4003, BurnBootloader: 4004, + UploadRequiresProgrammer: 4005, }; export const VerifyFailed = declareCoreError(Codes.Verify); export const UploadFailed = declareCoreError(Codes.Upload); @@ -78,6 +79,10 @@ export namespace CoreError { Codes.UploadUsingProgrammer ); export const BurnBootloaderFailed = declareCoreError(Codes.BurnBootloader); + export const UploadRequiresProgrammer = declareCoreError( + Codes.UploadRequiresProgrammer + ); + export function is( error: unknown ): error is ApplicationError { diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index 9ae7b405e..b8eba0335 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -1,4 +1,4 @@ -import type { ClientReadableStream } from '@grpc/grpc-js'; +import { type ClientReadableStream } from '@grpc/grpc-js'; import { ApplicationError } from '@theia/core/lib/common/application-error'; import type { CancellationToken } from '@theia/core/lib/common/cancellation'; import { CommandService } from '@theia/core/lib/common/command'; @@ -41,6 +41,7 @@ import { Port as RpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_ import { BurnBootloaderRequest, BurnBootloaderResponse, + ProgrammerIsRequiredForUploadError, UploadRequest, UploadResponse, UploadUsingProgrammerRequest, @@ -295,12 +296,24 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { reject(UserAbortApplicationError()); return; } + + if ( + ServiceError.isInstanceOf( + error, + ProgrammerIsRequiredForUploadError + ) + ) { + reject(CoreError.UploadRequiresProgrammer()); + return; + } + const message = nls.localize( 'arduino/upload/error', '{0} error: {1}', firstToUpperCase(task), error.details ); + this.sendResponse(error.details, OutputMessage.Severity.Error); reject( errorCtor(