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,48 +90,84 @@ 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()) console.error(
'Unexpected error occurred while compiling the sketch.',
error
); );
} reject(error);
call };
.on('data', handler.onData)
.on('error', (error) => { const handleCancellationError = () => {
if (!ServiceError.is(error)) { console.log(userAbort);
console.error( reject(UserAbortApplicationError());
'Unexpected error occurred while compiling the sketch.', };
error
); const handleInstanceNeedsReinitializationError = async (
reject(error); error: ServiceError & InstanceNeedsReinitializationError
return; ) => {
} if (hasRetried) {
if (ServiceError.isCancel(error)) { // If error persists, send the error message to the output
console.log(userAbort); return parseAndSendErrorResponse(error);
reject(UserAbortApplicationError()); }
return;
} hasRetried = true;
const compilerErrors = tryParseError({ await this.refresh();
content: handler.content, return startCompileStream();
sketch: options.sketch, };
});
const message = nls.localize( const parseAndSendErrorResponse = (error: ServiceError) => {
'arduino/compile/error', const compilerErrors = tryParseError({
'Compilation error: {0}', content: handler.content,
compilerErrors sketch: options.sketch,
.map(({ message }) => message) });
.filter(notEmpty) const message = nls.localize(
.shift() ?? error.details '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())
); );
this.sendResponse( }
error.details + '\n\n' + message,
OutputMessage.Severity.Error call
); .on('data', handler.onData)
reject(CoreError.VerifyFailed(message, compilerErrors)); .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);
} }