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 {
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<void>((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
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())
);
this.sendResponse(
error.details + '\n\n' + message,
OutputMessage.Severity.Error
);
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)) {

View File

@ -1,7 +1,9 @@
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';
import { InstanceNeedsReinitializationError } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb';
type ProtoError = typeof ProgrammerIsRequiredForUploadError;
const protoErrorsMap = new Map<string, ProtoError>([
@ -9,15 +11,27 @@ const protoErrorsMap = new Map<string, ProtoError>([
'cc.arduino.cli.commands.v1.ProgrammerIsRequiredForUploadError',
ProgrammerIsRequiredForUploadError,
],
[
'cc.arduino.cli.commands.v1.InstanceNeedsReinitializationError',
InstanceNeedsReinitializationError,
],
// handle other cli defined errors here
]);
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
}
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);
}