From d3306e0577bbd59828c24392efe00d0f1f042a48 Mon Sep 17 00:00:00 2001 From: Giacomo Cusinato <7659518+giacomocusinato@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:59:58 +0100 Subject: [PATCH 1/3] fix: use `Status` enum for status code in `ServiceError` type guards This change resolves the issue where the intersection of `ServiceError` error codes of type `number` resulted in the `never` type due to conflict between number and `State` enum if `StatusObject` --- arduino-ide-extension/src/node/service-error.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/arduino-ide-extension/src/node/service-error.ts b/arduino-ide-extension/src/node/service-error.ts index 0b6021ff0..b2c82a5d2 100644 --- a/arduino-ide-extension/src/node/service-error.ts +++ b/arduino-ide-extension/src/node/service-error.ts @@ -1,6 +1,7 @@ import { Metadata, StatusObject } from '@grpc/grpc-js'; import { Status } from './cli-protocol/google/rpc/status_pb'; import { stringToUint8Array } from '../common/utils'; +import { Status as StatusCode } from '@grpc/grpc-js/build/src/constants'; import { ProgrammerIsRequiredForUploadError } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; type ProtoError = typeof ProgrammerIsRequiredForUploadError; @@ -14,7 +15,9 @@ const protoErrorsMap = new Map([ export type ServiceError = StatusObject & Error; export namespace ServiceError { - export function isCancel(arg: unknown): arg is ServiceError & { code: 1 } { + export function isCancel( + arg: unknown + ): arg is ServiceError & { code: StatusCode.CANCELLED } { return is(arg) && arg.code === 1; // https://grpc.github.io/grpc/core/md_doc_statuscodes.html } From a52bce2fe308ec096a8035a57eb172607e98fe4e Mon Sep 17 00:00:00 2001 From: Giacomo Cusinato <7659518+giacomocusinato@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:00:39 +0100 Subject: [PATCH 2/3] feat: add `isInvalidArgument` type guard to `ServiceError` --- arduino-ide-extension/src/node/service-error.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/arduino-ide-extension/src/node/service-error.ts b/arduino-ide-extension/src/node/service-error.ts index b2c82a5d2..20c3dd059 100644 --- a/arduino-ide-extension/src/node/service-error.ts +++ b/arduino-ide-extension/src/node/service-error.ts @@ -21,6 +21,12 @@ export namespace ServiceError { return is(arg) && arg.code === 1; // https://grpc.github.io/grpc/core/md_doc_statuscodes.html } + export function isInvalidArgument( + arg: unknown + ): arg is ServiceError & { code: StatusCode.INVALID_ARGUMENT } { + return is(arg) && arg.code === 3; // https://grpc.github.io/grpc/core/md_doc_statuscodes.html + } + export function is(arg: unknown): arg is ServiceError { return arg instanceof Error && isStatusObject(arg); } From 7bc2a1ef0370759a50678b2d4fa5338f10253bed Mon Sep 17 00:00:00 2001 From: Giacomo Cusinato <7659518+giacomocusinato@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:42:54 +0200 Subject: [PATCH 3/3] fix: retry compilation if grpc client needs to be reinitialized See https://github.com/arduino/arduino-ide/issues/2547 --- .../src/node/core-service-impl.ts | 115 ++++++++++++------ .../src/node/service-error.ts | 5 + 2 files changed, 81 insertions(+), 39 deletions(-) diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index b8eba0335..285c05f72 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -36,6 +36,7 @@ import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; import { CompileRequest, CompileResponse, + InstanceNeedsReinitializationError, } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb'; import { Port as RpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb'; import { @@ -89,48 +90,84 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { compileSummaryHandler ); const toDisposeOnFinally = new DisposableCollection(handler); + return new Promise((resolve, reject) => { - const call = client.compile(request); - if (cancellationToken) { - toDisposeOnFinally.push( - cancellationToken.onCancellationRequested(() => call.cancel()) + let hasRetried = false; + + const handleUnexpectedError = (error: Error) => { + console.error( + 'Unexpected error occurred while compiling the sketch.', + error ); - } - call - .on('data', handler.onData) - .on('error', (error) => { - if (!ServiceError.is(error)) { - console.error( - 'Unexpected error occurred while compiling the sketch.', - error - ); - reject(error); - return; - } - if (ServiceError.isCancel(error)) { - console.log(userAbort); - reject(UserAbortApplicationError()); - return; - } - const compilerErrors = tryParseError({ - content: handler.content, - sketch: options.sketch, - }); - const message = nls.localize( - 'arduino/compile/error', - 'Compilation error: {0}', - compilerErrors - .map(({ message }) => message) - .filter(notEmpty) - .shift() ?? error.details - ); - this.sendResponse( - error.details + '\n\n' + message, - OutputMessage.Severity.Error + reject(error); + }; + + const handleCancellationError = () => { + console.log(userAbort); + reject(UserAbortApplicationError()); + }; + + const handleInstanceNeedsReinitializationError = async ( + error: ServiceError & InstanceNeedsReinitializationError + ) => { + if (hasRetried) { + // If error persists, send the error message to the output + return parseAndSendErrorResponse(error); + } + + hasRetried = true; + await this.refresh(); + return startCompileStream(); + }; + + const parseAndSendErrorResponse = (error: ServiceError) => { + const compilerErrors = tryParseError({ + content: handler.content, + sketch: options.sketch, + }); + const message = nls.localize( + 'arduino/compile/error', + 'Compilation error: {0}', + compilerErrors + .map(({ message }) => message) + .filter(notEmpty) + .shift() ?? error.details + ); + this.sendResponse( + error.details + '\n\n' + message, + OutputMessage.Severity.Error + ); + reject(CoreError.VerifyFailed(message, compilerErrors)); + }; + + const handleError = async (error: Error) => { + if (!ServiceError.is(error)) return handleUnexpectedError(error); + if (ServiceError.isCancel(error)) return handleCancellationError(); + + if ( + ServiceError.isInstanceOf(error, InstanceNeedsReinitializationError) + ) { + return await handleInstanceNeedsReinitializationError(error); + } + + parseAndSendErrorResponse(error); + }; + + const startCompileStream = () => { + const call = client.compile(request); + if (cancellationToken) { + toDisposeOnFinally.push( + cancellationToken.onCancellationRequested(() => call.cancel()) ); - reject(CoreError.VerifyFailed(message, compilerErrors)); - }) - .on('end', resolve); + } + + call + .on('data', handler.onData) + .on('error', handleError) + .on('end', resolve); + }; + + startCompileStream(); }).finally(() => { toDisposeOnFinally.dispose(); if (!isCompileSummary(compileSummary)) { diff --git a/arduino-ide-extension/src/node/service-error.ts b/arduino-ide-extension/src/node/service-error.ts index 20c3dd059..681896a03 100644 --- a/arduino-ide-extension/src/node/service-error.ts +++ b/arduino-ide-extension/src/node/service-error.ts @@ -3,6 +3,7 @@ import { Status } from './cli-protocol/google/rpc/status_pb'; import { stringToUint8Array } from '../common/utils'; import { Status as StatusCode } from '@grpc/grpc-js/build/src/constants'; import { ProgrammerIsRequiredForUploadError } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; +import { InstanceNeedsReinitializationError } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb'; type ProtoError = typeof ProgrammerIsRequiredForUploadError; const protoErrorsMap = new Map([ @@ -10,6 +11,10 @@ const protoErrorsMap = new Map([ 'cc.arduino.cli.commands.v1.ProgrammerIsRequiredForUploadError', ProgrammerIsRequiredForUploadError, ], + [ + 'cc.arduino.cli.commands.v1.InstanceNeedsReinitializationError', + InstanceNeedsReinitializationError, + ], // handle other cli defined errors here ]);