fix: retry compilation if grpc client needs to be reinitialized (#2548)

* 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`

* feat: add `isInvalidArgument` type guard to `ServiceError`

* fix: retry compilation if grpc client needs to be reinitialized

See https://github.com/arduino/arduino-ide/issues/2547
This commit is contained in:
Giacomo Cusinato 2024-11-21 08:42:14 +01:00 committed by GitHub
parent 41844c9470
commit 4cf9909a07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 91 additions and 40 deletions

View File

@ -36,6 +36,7 @@ import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
import { import {
CompileRequest, CompileRequest,
CompileResponse, CompileResponse,
InstanceNeedsReinitializationError,
} from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb'; } 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 { Port as RpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
import { import {
@ -89,29 +90,37 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
compileSummaryHandler compileSummaryHandler
); );
const toDisposeOnFinally = new DisposableCollection(handler); const toDisposeOnFinally = new DisposableCollection(handler);
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const call = client.compile(request); let hasRetried = false;
if (cancellationToken) {
toDisposeOnFinally.push( const handleUnexpectedError = (error: Error) => {
cancellationToken.onCancellationRequested(() => call.cancel())
);
}
call
.on('data', handler.onData)
.on('error', (error) => {
if (!ServiceError.is(error)) {
console.error( console.error(
'Unexpected error occurred while compiling the sketch.', 'Unexpected error occurred while compiling the sketch.',
error error
); );
reject(error); reject(error);
return; };
}
if (ServiceError.isCancel(error)) { const handleCancellationError = () => {
console.log(userAbort); console.log(userAbort);
reject(UserAbortApplicationError()); reject(UserAbortApplicationError());
return; };
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({ const compilerErrors = tryParseError({
content: handler.content, content: handler.content,
sketch: options.sketch, sketch: options.sketch,
@ -129,8 +138,36 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
OutputMessage.Severity.Error OutputMessage.Severity.Error
); );
reject(CoreError.VerifyFailed(message, compilerErrors)); 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())
);
}
call
.on('data', handler.onData)
.on('error', handleError)
.on('end', resolve); .on('end', resolve);
};
startCompileStream();
}).finally(() => { }).finally(() => {
toDisposeOnFinally.dispose(); toDisposeOnFinally.dispose();
if (!isCompileSummary(compileSummary)) { if (!isCompileSummary(compileSummary)) {

View File

@ -1,7 +1,9 @@
import { Metadata, StatusObject } from '@grpc/grpc-js'; import { Metadata, StatusObject } from '@grpc/grpc-js';
import { Status } from './cli-protocol/google/rpc/status_pb'; import { Status } from './cli-protocol/google/rpc/status_pb';
import { stringToUint8Array } from '../common/utils'; 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 { 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; type ProtoError = typeof ProgrammerIsRequiredForUploadError;
const protoErrorsMap = new Map<string, ProtoError>([ const protoErrorsMap = new Map<string, ProtoError>([
@ -9,15 +11,27 @@ const protoErrorsMap = new Map<string, ProtoError>([
'cc.arduino.cli.commands.v1.ProgrammerIsRequiredForUploadError', 'cc.arduino.cli.commands.v1.ProgrammerIsRequiredForUploadError',
ProgrammerIsRequiredForUploadError, ProgrammerIsRequiredForUploadError,
], ],
[
'cc.arduino.cli.commands.v1.InstanceNeedsReinitializationError',
InstanceNeedsReinitializationError,
],
// handle other cli defined errors here // handle other cli defined errors here
]); ]);
export type ServiceError = StatusObject & Error; export type ServiceError = StatusObject & Error;
export namespace ServiceError { 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 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 { export function is(arg: unknown): arg is ServiceError {
return arg instanceof Error && isStatusObject(arg); return arg instanceof Error && isStatusObject(arg);
} }