mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-22 00:26:09 +00:00
Reveal the error location after on failed verify.
Closes #608 Closes #229 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
234
arduino-ide-extension/src/node/cli-error-parser.ts
Normal file
234
arduino-ide-extension/src/node/cli-error-parser.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { notEmpty } from '@theia/core';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||
import {
|
||||
Location,
|
||||
Range,
|
||||
Position,
|
||||
} from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import { Sketch } from '../common/protocol';
|
||||
|
||||
export interface ErrorInfo {
|
||||
readonly message?: string;
|
||||
readonly location?: Location;
|
||||
readonly details?: string;
|
||||
}
|
||||
export interface ErrorSource {
|
||||
readonly content: string | ReadonlyArray<Uint8Array>;
|
||||
readonly sketch?: Sketch;
|
||||
}
|
||||
|
||||
export function tryParseError(source: ErrorSource): ErrorInfo[] {
|
||||
const { content, sketch } = source;
|
||||
const err =
|
||||
typeof content === 'string'
|
||||
? content
|
||||
: Buffer.concat(content).toString('utf8');
|
||||
if (sketch) {
|
||||
return tryParse(err)
|
||||
.map(remapErrorMessages)
|
||||
.filter(isLocationInSketch(sketch))
|
||||
.map(errorInfo());
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
interface ParseResult {
|
||||
readonly path: string;
|
||||
readonly line: number;
|
||||
readonly column?: number;
|
||||
readonly errorPrefix: string;
|
||||
readonly error: string;
|
||||
readonly message?: string;
|
||||
}
|
||||
namespace ParseResult {
|
||||
export function keyOf(result: ParseResult): string {
|
||||
/**
|
||||
* The CLI compiler might return with the same error multiple times. This is the key function for the distinct set calculation.
|
||||
*/
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
}
|
||||
|
||||
function isLocationInSketch(
|
||||
sketch: Sketch
|
||||
): (value: ParseResult, index: number, array: ParseResult[]) => unknown {
|
||||
return (result) => {
|
||||
const uri = FileUri.create(result.path).toString();
|
||||
if (!Sketch.isInSketch(uri, sketch)) {
|
||||
console.warn(
|
||||
`URI <${uri}> is not contained in sketch: <${JSON.stringify(sketch)}>`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
function errorInfo(): (value: ParseResult) => ErrorInfo {
|
||||
return ({ error, message, path, line, column }) => ({
|
||||
message: error,
|
||||
details: message,
|
||||
location: {
|
||||
uri: FileUri.create(path).toString(),
|
||||
range: range(line, column),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function range(line: number, column?: number): Range {
|
||||
const start = Position.create(
|
||||
line - 1,
|
||||
typeof column === 'number' ? column - 1 : 0
|
||||
);
|
||||
return {
|
||||
start,
|
||||
end: start,
|
||||
};
|
||||
}
|
||||
|
||||
export function tryParse(raw: string): ParseResult[] {
|
||||
// Shamelessly stolen from the Java IDE: https://github.com/arduino/Arduino/blob/43b0818f7fa8073301db1b80ac832b7b7596b828/arduino-core/src/cc/arduino/Compiler.java#L137
|
||||
const re = new RegExp(
|
||||
'(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*((fatal)?\\s*error:\\s*)(.*)\\s*',
|
||||
'gm'
|
||||
);
|
||||
return [
|
||||
...new Map(
|
||||
Array.from(raw.matchAll(re) ?? [])
|
||||
.map((match) => {
|
||||
const [, path, rawLine, rawColumn, errorPrefix, , error] = match.map(
|
||||
(match) => (match ? match.trim() : match)
|
||||
);
|
||||
const line = Number.parseInt(rawLine, 10);
|
||||
if (!Number.isInteger(line)) {
|
||||
console.warn(
|
||||
`Could not parse line number. Raw input: <${rawLine}>, parsed integer: <${line}>.`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
let column: number | undefined = undefined;
|
||||
if (rawColumn) {
|
||||
const normalizedRawColumn = rawColumn.slice(-1); // trims the leading colon => `:3` will be `3`
|
||||
column = Number.parseInt(normalizedRawColumn, 10);
|
||||
if (!Number.isInteger(column)) {
|
||||
console.warn(
|
||||
`Could not parse column number. Raw input: <${normalizedRawColumn}>, parsed integer: <${column}>.`
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
path,
|
||||
line,
|
||||
column,
|
||||
errorPrefix,
|
||||
error,
|
||||
};
|
||||
})
|
||||
.filter(notEmpty)
|
||||
.map((result) => [ParseResult.keyOf(result), result])
|
||||
).values(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts cryptic and legacy error messages to nice ones. Taken from the Java IDE.
|
||||
*/
|
||||
function remapErrorMessages(result: ParseResult): ParseResult {
|
||||
const knownError = KnownErrors[result.error];
|
||||
if (!knownError) {
|
||||
return result;
|
||||
}
|
||||
const { message, error } = knownError;
|
||||
return {
|
||||
...result,
|
||||
...(message && { message }),
|
||||
...(error && { error }),
|
||||
};
|
||||
}
|
||||
|
||||
// Based on the Java IDE: https://github.com/arduino/Arduino/blob/43b0818f7fa8073301db1b80ac832b7b7596b828/arduino-core/src/cc/arduino/Compiler.java#L528-L578
|
||||
const KnownErrors: Record<string, { error: string; message?: string }> = {
|
||||
'SPI.h: No such file or directory': {
|
||||
error: nls.localize(
|
||||
'arduino/cli-error-parser/spiError',
|
||||
'Please import the SPI library from the Sketch > Import Library menu.'
|
||||
),
|
||||
message: nls.localize(
|
||||
'arduino/cli-error-parser/spiMessage',
|
||||
'As of Arduino 0019, the Ethernet library depends on the SPI library.\nYou appear to be using it or another library that depends on the SPI library.'
|
||||
),
|
||||
},
|
||||
"'BYTE' was not declared in this scope": {
|
||||
error: nls.localize(
|
||||
'arduino/cli-error-parser/byteError',
|
||||
"The 'BYTE' keyword is no longer supported."
|
||||
),
|
||||
message: nls.localize(
|
||||
'arduino/cli-error-parser/byteMessage',
|
||||
"As of Arduino 1.0, the 'BYTE' keyword is no longer supported.\nPlease use Serial.write() instead."
|
||||
),
|
||||
},
|
||||
"no matching function for call to 'Server::Server(int)'": {
|
||||
error: nls.localize(
|
||||
'arduino/cli-error-parser/serverError',
|
||||
'The Server class has been renamed EthernetServer.'
|
||||
),
|
||||
message: nls.localize(
|
||||
'arduino/cli-error-parser/serverMessage',
|
||||
'As of Arduino 1.0, the Server class in the Ethernet library has been renamed to EthernetServer.'
|
||||
),
|
||||
},
|
||||
"no matching function for call to 'Client::Client(byte [4], int)'": {
|
||||
error: nls.localize(
|
||||
'arduino/cli-error-parser/clientError',
|
||||
'The Client class has been renamed EthernetClient.'
|
||||
),
|
||||
message: nls.localize(
|
||||
'arduino/cli-error-parser/clientMessage',
|
||||
'As of Arduino 1.0, the Client class in the Ethernet library has been renamed to EthernetClient.'
|
||||
),
|
||||
},
|
||||
"'Udp' was not declared in this scope": {
|
||||
error: nls.localize(
|
||||
'arduino/cli-error-parser/udpError',
|
||||
'The Udp class has been renamed EthernetUdp.'
|
||||
),
|
||||
message: nls.localize(
|
||||
'arduino/cli-error-parser/udpMessage',
|
||||
'As of Arduino 1.0, the Udp class in the Ethernet library has been renamed to EthernetUdp.'
|
||||
),
|
||||
},
|
||||
"'class TwoWire' has no member named 'send'": {
|
||||
error: nls.localize(
|
||||
'arduino/cli-error-parser/sendError',
|
||||
'Wire.send() has been renamed Wire.write().'
|
||||
),
|
||||
message: nls.localize(
|
||||
'arduino/cli-error-parser/sendMessage',
|
||||
'As of Arduino 1.0, the Wire.send() function was renamed to Wire.write() for consistency with other libraries.'
|
||||
),
|
||||
},
|
||||
"'class TwoWire' has no member named 'receive'": {
|
||||
error: nls.localize(
|
||||
'arduino/cli-error-parser/receiveError',
|
||||
'Wire.receive() has been renamed Wire.read().'
|
||||
),
|
||||
message: nls.localize(
|
||||
'arduino/cli-error-parser/receiveMessage',
|
||||
'As of Arduino 1.0, the Wire.receive() function was renamed to Wire.read() for consistency with other libraries.'
|
||||
),
|
||||
},
|
||||
"'Mouse' was not declared in this scope": {
|
||||
error: nls.localize(
|
||||
'arduino/cli-error-parser/mouseError',
|
||||
"'Mouse' not found. Does your sketch include the line '#include <Mouse.h>'?"
|
||||
),
|
||||
},
|
||||
"'Keyboard' was not declared in this scope": {
|
||||
error: nls.localize(
|
||||
'arduino/cli-error-parser/keyboardError',
|
||||
"'Keyboard' not found. Does your sketch include the line '#include <Keyboard.h>'?"
|
||||
),
|
||||
},
|
||||
};
|
||||
@@ -4,7 +4,11 @@ import { relative } from 'path';
|
||||
import * as jspb from 'google-protobuf';
|
||||
import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb';
|
||||
import { ClientReadableStream } from '@grpc/grpc-js';
|
||||
import { CompilerWarnings, CoreService } from '../common/protocol/core-service';
|
||||
import {
|
||||
CompilerWarnings,
|
||||
CoreService,
|
||||
CoreError,
|
||||
} from '../common/protocol/core-service';
|
||||
import {
|
||||
CompileRequest,
|
||||
CompileResponse,
|
||||
@@ -19,27 +23,24 @@ import {
|
||||
UploadUsingProgrammerResponse,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
|
||||
import { ResponseService } from '../common/protocol/response-service';
|
||||
import { NotificationServiceServer } from '../common/protocol';
|
||||
import { Board, OutputMessage, Port, Status } from '../common/protocol';
|
||||
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
|
||||
import { firstToUpperCase, firstToLowerCase } from '../common/utils';
|
||||
import { Port } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
|
||||
import { nls } from '@theia/core';
|
||||
import { Port as GrpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
|
||||
import { ApplicationError, Disposable, nls } from '@theia/core';
|
||||
import { MonitorManager } from './monitor-manager';
|
||||
import { SimpleBuffer } from './utils/simple-buffer';
|
||||
import { tryParseError } from './cli-error-parser';
|
||||
import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
|
||||
import { firstToUpperCase, notEmpty } from '../common/utils';
|
||||
import { ServiceError } from './service-error';
|
||||
|
||||
const FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS = 32;
|
||||
@injectable()
|
||||
export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
@inject(ResponseService)
|
||||
protected readonly responseService: ResponseService;
|
||||
|
||||
@inject(NotificationServiceServer)
|
||||
protected readonly notificationService: NotificationServiceServer;
|
||||
private readonly responseService: ResponseService;
|
||||
|
||||
@inject(MonitorManager)
|
||||
protected readonly monitorManager: MonitorManager;
|
||||
|
||||
protected uploading = false;
|
||||
private readonly monitorManager: MonitorManager;
|
||||
|
||||
async compile(
|
||||
options: CoreService.Compile.Options & {
|
||||
@@ -47,254 +48,298 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
compilerWarnings?: CompilerWarnings;
|
||||
}
|
||||
): Promise<void> {
|
||||
const { sketchUri, board, compilerWarnings } = options;
|
||||
const sketchPath = FileUri.fsPath(sketchUri);
|
||||
|
||||
await this.coreClientProvider.initialized;
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
const handler = this.createOnDataHandler();
|
||||
const request = this.compileRequest(options, instance);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client
|
||||
.compile(request)
|
||||
.on('data', handler.onData)
|
||||
.on('error', (error) => {
|
||||
if (!ServiceError.is(error)) {
|
||||
console.error(
|
||||
'Unexpected error occurred while compiling the sketch.',
|
||||
error
|
||||
);
|
||||
reject(error);
|
||||
} else {
|
||||
const compilerErrors = tryParseError({
|
||||
content: handler.stderr,
|
||||
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));
|
||||
}
|
||||
})
|
||||
.on('end', resolve);
|
||||
}).finally(() => handler.dispose());
|
||||
}
|
||||
|
||||
const compileReq = new CompileRequest();
|
||||
compileReq.setInstance(instance);
|
||||
compileReq.setSketchPath(sketchPath);
|
||||
private compileRequest(
|
||||
options: CoreService.Compile.Options & {
|
||||
exportBinaries?: boolean;
|
||||
compilerWarnings?: CompilerWarnings;
|
||||
},
|
||||
instance: Instance
|
||||
): CompileRequest {
|
||||
const { sketch, board, compilerWarnings } = options;
|
||||
const sketchUri = sketch.uri;
|
||||
const sketchPath = FileUri.fsPath(sketchUri);
|
||||
const request = new CompileRequest();
|
||||
request.setInstance(instance);
|
||||
request.setSketchPath(sketchPath);
|
||||
if (board?.fqbn) {
|
||||
compileReq.setFqbn(board.fqbn);
|
||||
request.setFqbn(board.fqbn);
|
||||
}
|
||||
if (compilerWarnings) {
|
||||
compileReq.setWarnings(compilerWarnings.toLowerCase());
|
||||
request.setWarnings(compilerWarnings.toLowerCase());
|
||||
}
|
||||
compileReq.setOptimizeForDebug(options.optimizeForDebug);
|
||||
compileReq.setPreprocess(false);
|
||||
compileReq.setVerbose(options.verbose);
|
||||
compileReq.setQuiet(false);
|
||||
request.setOptimizeForDebug(options.optimizeForDebug);
|
||||
request.setPreprocess(false);
|
||||
request.setVerbose(options.verbose);
|
||||
request.setQuiet(false);
|
||||
if (typeof options.exportBinaries === 'boolean') {
|
||||
const exportBinaries = new BoolValue();
|
||||
exportBinaries.setValue(options.exportBinaries);
|
||||
compileReq.setExportBinaries(exportBinaries);
|
||||
}
|
||||
this.mergeSourceOverrides(compileReq, options);
|
||||
|
||||
const result = client.compile(compileReq);
|
||||
|
||||
const compileBuffer = new SimpleBuffer(
|
||||
this.flushOutputPanelMessages.bind(this),
|
||||
FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS
|
||||
);
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
result.on('data', (cr: CompileResponse) => {
|
||||
compileBuffer.addChunk(cr.getOutStream_asU8());
|
||||
compileBuffer.addChunk(cr.getErrStream_asU8());
|
||||
});
|
||||
result.on('error', (error) => {
|
||||
compileBuffer.clearFlushInterval();
|
||||
reject(error);
|
||||
});
|
||||
result.on('end', () => {
|
||||
compileBuffer.clearFlushInterval();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
this.responseService.appendToOutput({
|
||||
chunk: '\n--------------------------\nCompilation complete.\n',
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage = nls.localize(
|
||||
'arduino/compile/error',
|
||||
'Compilation error: {0}',
|
||||
e.details
|
||||
);
|
||||
this.responseService.appendToOutput({
|
||||
chunk: `${errorMessage}\n`,
|
||||
severity: 'error',
|
||||
});
|
||||
throw new Error(errorMessage);
|
||||
request.setExportBinaries(exportBinaries);
|
||||
}
|
||||
this.mergeSourceOverrides(request, options);
|
||||
return request;
|
||||
}
|
||||
|
||||
async upload(options: CoreService.Upload.Options): Promise<void> {
|
||||
await this.doUpload(
|
||||
return this.doUpload(
|
||||
options,
|
||||
() => new UploadRequest(),
|
||||
(client, req) => client.upload(req)
|
||||
(client, req) => client.upload(req),
|
||||
(message: string, info: CoreError.ErrorInfo[]) =>
|
||||
CoreError.UploadFailed(message, info),
|
||||
'upload'
|
||||
);
|
||||
}
|
||||
|
||||
async uploadUsingProgrammer(
|
||||
options: CoreService.Upload.Options
|
||||
): Promise<void> {
|
||||
await this.doUpload(
|
||||
return this.doUpload(
|
||||
options,
|
||||
() => new UploadUsingProgrammerRequest(),
|
||||
(client, req) => client.uploadUsingProgrammer(req),
|
||||
(message: string, info: CoreError.ErrorInfo[]) =>
|
||||
CoreError.UploadUsingProgrammerFailed(message, info),
|
||||
'upload using programmer'
|
||||
);
|
||||
}
|
||||
|
||||
isUploading(): Promise<boolean> {
|
||||
return Promise.resolve(this.uploading);
|
||||
}
|
||||
|
||||
protected async doUpload(
|
||||
options: CoreService.Upload.Options,
|
||||
requestProvider: () => UploadRequest | UploadUsingProgrammerRequest,
|
||||
// tslint:disable-next-line:max-line-length
|
||||
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest,
|
||||
responseHandler: (
|
||||
client: ArduinoCoreServiceClient,
|
||||
req: UploadRequest | UploadUsingProgrammerRequest
|
||||
request: UploadRequest | UploadUsingProgrammerRequest
|
||||
) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
|
||||
task = 'upload'
|
||||
errorHandler: (
|
||||
message: string,
|
||||
info: CoreError.ErrorInfo[]
|
||||
) => ApplicationError<number, CoreError.ErrorInfo[]>,
|
||||
task: string
|
||||
): Promise<void> {
|
||||
await this.compile(Object.assign(options, { exportBinaries: false }));
|
||||
|
||||
this.uploading = true;
|
||||
const { sketchUri, board, port, programmer } = options;
|
||||
await this.monitorManager.notifyUploadStarted(board, port);
|
||||
|
||||
const sketchPath = FileUri.fsPath(sketchUri);
|
||||
|
||||
await this.coreClientProvider.initialized;
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
const request = this.uploadOrUploadUsingProgrammerRequest(
|
||||
options,
|
||||
instance,
|
||||
requestFactory
|
||||
);
|
||||
const handler = this.createOnDataHandler();
|
||||
return this.notifyUploadWillStart(options).then(() =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
responseHandler(client, request)
|
||||
.on('data', handler.onData)
|
||||
.on('error', (error) => {
|
||||
if (!ServiceError.is(error)) {
|
||||
console.error(`Unexpected error occurred while ${task}.`, error);
|
||||
reject(error);
|
||||
} else {
|
||||
const message = nls.localize(
|
||||
'arduino/upload/error',
|
||||
'{0} error: {1}',
|
||||
firstToUpperCase(task),
|
||||
error.details
|
||||
);
|
||||
this.sendResponse(error.details, OutputMessage.Severity.Error);
|
||||
reject(
|
||||
errorHandler(
|
||||
message,
|
||||
tryParseError({
|
||||
content: handler.stderr,
|
||||
sketch: options.sketch,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
.on('end', resolve);
|
||||
}).finally(async () => {
|
||||
handler.dispose();
|
||||
await this.notifyUploadDidFinish(options);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const req = requestProvider();
|
||||
req.setInstance(instance);
|
||||
req.setSketchPath(sketchPath);
|
||||
private uploadOrUploadUsingProgrammerRequest(
|
||||
options: CoreService.Upload.Options,
|
||||
instance: Instance,
|
||||
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest
|
||||
): UploadRequest | UploadUsingProgrammerRequest {
|
||||
const { sketch, board, port, programmer } = options;
|
||||
const sketchPath = FileUri.fsPath(sketch.uri);
|
||||
const request = requestFactory();
|
||||
request.setInstance(instance);
|
||||
request.setSketchPath(sketchPath);
|
||||
if (board?.fqbn) {
|
||||
req.setFqbn(board.fqbn);
|
||||
request.setFqbn(board.fqbn);
|
||||
}
|
||||
const p = new Port();
|
||||
if (port) {
|
||||
p.setAddress(port.address);
|
||||
p.setLabel(port.addressLabel);
|
||||
p.setProtocol(port.protocol);
|
||||
p.setProtocolLabel(port.protocolLabel);
|
||||
}
|
||||
req.setPort(p);
|
||||
request.setPort(this.createPort(port));
|
||||
if (programmer) {
|
||||
req.setProgrammer(programmer.id);
|
||||
request.setProgrammer(programmer.id);
|
||||
}
|
||||
req.setVerbose(options.verbose);
|
||||
req.setVerify(options.verify);
|
||||
request.setVerbose(options.verbose);
|
||||
request.setVerify(options.verify);
|
||||
|
||||
options.userFields.forEach((e) => {
|
||||
req.getUserFieldsMap().set(e.name, e.value);
|
||||
request.getUserFieldsMap().set(e.name, e.value);
|
||||
});
|
||||
|
||||
const result = responseHandler(client, req);
|
||||
|
||||
const uploadBuffer = new SimpleBuffer(
|
||||
this.flushOutputPanelMessages.bind(this),
|
||||
FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS
|
||||
);
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
result.on('data', (resp: UploadResponse) => {
|
||||
uploadBuffer.addChunk(resp.getOutStream_asU8());
|
||||
uploadBuffer.addChunk(resp.getErrStream_asU8());
|
||||
});
|
||||
result.on('error', (error) => {
|
||||
uploadBuffer.clearFlushInterval();
|
||||
reject(error);
|
||||
});
|
||||
result.on('end', () => {
|
||||
uploadBuffer.clearFlushInterval();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
this.responseService.appendToOutput({
|
||||
chunk:
|
||||
'\n--------------------------\n' +
|
||||
firstToLowerCase(task) +
|
||||
' complete.\n',
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage = nls.localize(
|
||||
'arduino/upload/error',
|
||||
'{0} error: {1}',
|
||||
firstToUpperCase(task),
|
||||
e.details
|
||||
);
|
||||
this.responseService.appendToOutput({
|
||||
chunk: `${errorMessage}\n`,
|
||||
severity: 'error',
|
||||
});
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
this.uploading = false;
|
||||
this.monitorManager.notifyUploadFinished(board, port);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
|
||||
this.uploading = true;
|
||||
const { board, port, programmer } = options;
|
||||
await this.monitorManager.notifyUploadStarted(board, port);
|
||||
|
||||
await this.coreClientProvider.initialized;
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
const burnReq = new BurnBootloaderRequest();
|
||||
burnReq.setInstance(instance);
|
||||
if (board?.fqbn) {
|
||||
burnReq.setFqbn(board.fqbn);
|
||||
}
|
||||
const p = new Port();
|
||||
if (port) {
|
||||
p.setAddress(port.address);
|
||||
p.setLabel(port.addressLabel);
|
||||
p.setProtocol(port.protocol);
|
||||
p.setProtocolLabel(port.protocolLabel);
|
||||
}
|
||||
burnReq.setPort(p);
|
||||
if (programmer) {
|
||||
burnReq.setProgrammer(programmer.id);
|
||||
}
|
||||
burnReq.setVerify(options.verify);
|
||||
burnReq.setVerbose(options.verbose);
|
||||
const result = client.burnBootloader(burnReq);
|
||||
|
||||
const bootloaderBuffer = new SimpleBuffer(
|
||||
this.flushOutputPanelMessages.bind(this),
|
||||
FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS
|
||||
const handler = this.createOnDataHandler();
|
||||
const request = this.burnBootloaderRequest(options, instance);
|
||||
return this.notifyUploadWillStart(options).then(() =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
client
|
||||
.burnBootloader(request)
|
||||
.on('data', handler.onData)
|
||||
.on('error', (error) => {
|
||||
if (!ServiceError.is(error)) {
|
||||
console.error(
|
||||
'Unexpected error occurred while burning the bootloader.',
|
||||
error
|
||||
);
|
||||
reject(error);
|
||||
} else {
|
||||
this.sendResponse(error.details, OutputMessage.Severity.Error);
|
||||
reject(
|
||||
CoreError.BurnBootloaderFailed(
|
||||
nls.localize(
|
||||
'arduino/burnBootloader/error',
|
||||
'Error while burning the bootloader: {0}',
|
||||
error.details
|
||||
),
|
||||
tryParseError({ content: handler.stderr })
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
.on('end', resolve);
|
||||
}).finally(async () => {
|
||||
handler.dispose();
|
||||
await this.notifyUploadDidFinish(options);
|
||||
})
|
||||
);
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
result.on('data', (resp: BurnBootloaderResponse) => {
|
||||
bootloaderBuffer.addChunk(resp.getOutStream_asU8());
|
||||
bootloaderBuffer.addChunk(resp.getErrStream_asU8());
|
||||
});
|
||||
result.on('error', (error) => {
|
||||
bootloaderBuffer.clearFlushInterval();
|
||||
reject(error);
|
||||
});
|
||||
result.on('end', () => {
|
||||
bootloaderBuffer.clearFlushInterval();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage = nls.localize(
|
||||
'arduino/burnBootloader/error',
|
||||
'Error while burning the bootloader: {0}',
|
||||
e.details
|
||||
);
|
||||
this.responseService.appendToOutput({
|
||||
chunk: `${errorMessage}\n`,
|
||||
severity: 'error',
|
||||
});
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
this.uploading = false;
|
||||
await this.monitorManager.notifyUploadFinished(board, port);
|
||||
}
|
||||
|
||||
private burnBootloaderRequest(
|
||||
options: CoreService.Bootloader.Options,
|
||||
instance: Instance
|
||||
): BurnBootloaderRequest {
|
||||
const { board, port, programmer } = options;
|
||||
const request = new BurnBootloaderRequest();
|
||||
request.setInstance(instance);
|
||||
if (board?.fqbn) {
|
||||
request.setFqbn(board.fqbn);
|
||||
}
|
||||
request.setPort(this.createPort(port));
|
||||
if (programmer) {
|
||||
request.setProgrammer(programmer.id);
|
||||
}
|
||||
request.setVerify(options.verify);
|
||||
request.setVerbose(options.verbose);
|
||||
return request;
|
||||
}
|
||||
|
||||
private createOnDataHandler<R extends StreamingResponse>(): Disposable & {
|
||||
stderr: Buffer[];
|
||||
onData: (response: R) => void;
|
||||
} {
|
||||
const stderr: Buffer[] = [];
|
||||
const buffer = new SimpleBuffer((chunks) => {
|
||||
Array.from(chunks.entries()).forEach(([severity, chunk]) => {
|
||||
if (chunk) {
|
||||
this.sendResponse(chunk, severity);
|
||||
}
|
||||
});
|
||||
});
|
||||
const onData = StreamingResponse.createOnDataHandler(stderr, (out, err) => {
|
||||
buffer.addChunk(out);
|
||||
buffer.addChunk(err, OutputMessage.Severity.Error);
|
||||
});
|
||||
return {
|
||||
dispose: () => buffer.dispose(),
|
||||
stderr,
|
||||
onData,
|
||||
};
|
||||
}
|
||||
|
||||
private sendResponse(
|
||||
chunk: string,
|
||||
severity: OutputMessage.Severity = OutputMessage.Severity.Info
|
||||
): void {
|
||||
this.responseService.appendToOutput({ chunk, severity });
|
||||
}
|
||||
|
||||
private async notifyUploadWillStart({
|
||||
board,
|
||||
port,
|
||||
}: {
|
||||
board?: Board | undefined;
|
||||
port?: Port | undefined;
|
||||
}): Promise<void> {
|
||||
return this.monitorManager.notifyUploadStarted(board, port);
|
||||
}
|
||||
|
||||
private async notifyUploadDidFinish({
|
||||
board,
|
||||
port,
|
||||
}: {
|
||||
board?: Board | undefined;
|
||||
port?: Port | undefined;
|
||||
}): Promise<Status> {
|
||||
return this.monitorManager.notifyUploadFinished(board, port);
|
||||
}
|
||||
|
||||
private mergeSourceOverrides(
|
||||
req: { getSourceOverrideMap(): jspb.Map<string, string> },
|
||||
options: CoreService.Compile.Options
|
||||
): void {
|
||||
const sketchPath = FileUri.fsPath(options.sketchUri);
|
||||
const sketchPath = FileUri.fsPath(options.sketch.uri);
|
||||
for (const uri of Object.keys(options.sourceOverride)) {
|
||||
const content = options.sourceOverride[uri];
|
||||
if (content) {
|
||||
@@ -304,9 +349,33 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
}
|
||||
}
|
||||
|
||||
private flushOutputPanelMessages(chunk: string): void {
|
||||
this.responseService.appendToOutput({
|
||||
chunk,
|
||||
});
|
||||
private createPort(port: Port | undefined): GrpcPort {
|
||||
const grpcPort = new GrpcPort();
|
||||
if (port) {
|
||||
grpcPort.setAddress(port.address);
|
||||
grpcPort.setLabel(port.addressLabel);
|
||||
grpcPort.setProtocol(port.protocol);
|
||||
grpcPort.setProtocolLabel(port.protocolLabel);
|
||||
}
|
||||
return grpcPort;
|
||||
}
|
||||
}
|
||||
type StreamingResponse =
|
||||
| CompileResponse
|
||||
| UploadResponse
|
||||
| UploadUsingProgrammerResponse
|
||||
| BurnBootloaderResponse;
|
||||
namespace StreamingResponse {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function createOnDataHandler<R extends StreamingResponse>(
|
||||
stderr: Uint8Array[],
|
||||
onData: (out: Uint8Array, err: Uint8Array) => void
|
||||
): (response: R) => void {
|
||||
return (response: R) => {
|
||||
const out = response.getOutStream_asU8();
|
||||
const err = response.getErrStream_asU8();
|
||||
stderr.push(err);
|
||||
onData(out, err);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,19 @@ import {
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { join, basename } from 'path';
|
||||
import { join } from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||
import {
|
||||
Sketch,
|
||||
SketchRef,
|
||||
SketchContainer,
|
||||
} from '../common/protocol/sketches-service';
|
||||
import { SketchesServiceImpl } from './sketches-service-impl';
|
||||
import { ExamplesService } from '../common/protocol/examples-service';
|
||||
import {
|
||||
LibraryLocation,
|
||||
LibraryPackage,
|
||||
LibraryService,
|
||||
} from '../common/protocol';
|
||||
import { ConfigServiceImpl } from './config-service-impl';
|
||||
import { duration } from '../common/decorators';
|
||||
import { URI } from '@theia/core/lib/common/uri';
|
||||
import { Path } from '@theia/core/lib/common/path';
|
||||
@@ -88,14 +84,8 @@ export class BuiltInExamplesServiceImpl {
|
||||
|
||||
@injectable()
|
||||
export class ExamplesServiceImpl implements ExamplesService {
|
||||
@inject(SketchesServiceImpl)
|
||||
protected readonly sketchesService: SketchesServiceImpl;
|
||||
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
|
||||
@inject(ConfigServiceImpl)
|
||||
protected readonly configService: ConfigServiceImpl;
|
||||
private readonly libraryService: LibraryService;
|
||||
|
||||
@inject(BuiltInExamplesServiceImpl)
|
||||
private readonly builtInExamplesService: BuiltInExamplesServiceImpl;
|
||||
@@ -117,7 +107,7 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
fqbn,
|
||||
});
|
||||
for (const pkg of packages) {
|
||||
const container = await this.tryGroupExamplesNew(pkg);
|
||||
const container = await this.tryGroupExamples(pkg);
|
||||
const { location } = pkg;
|
||||
if (location === LibraryLocation.USER) {
|
||||
user.push(container);
|
||||
@@ -130,9 +120,6 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
any.push(container);
|
||||
}
|
||||
}
|
||||
// user.sort((left, right) => left.label.localeCompare(right.label));
|
||||
// current.sort((left, right) => left.label.localeCompare(right.label));
|
||||
// any.sort((left, right) => left.label.localeCompare(right.label));
|
||||
return { user, current, any };
|
||||
}
|
||||
|
||||
@@ -141,7 +128,7 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
* folder hierarchy. This method tries to workaround it by falling back to the `installDirUri` and manually creating the
|
||||
* location of the examples. Otherwise it creates the example container from the direct examples FS paths.
|
||||
*/
|
||||
protected async tryGroupExamplesNew({
|
||||
private async tryGroupExamples({
|
||||
label,
|
||||
exampleUris,
|
||||
installDirUri,
|
||||
@@ -208,10 +195,6 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
if (!child) {
|
||||
child = SketchContainer.create(label);
|
||||
parent.children.push(child);
|
||||
//TODO: remove or move sort
|
||||
parent.children.sort((left, right) =>
|
||||
left.label.localeCompare(right.label)
|
||||
);
|
||||
}
|
||||
return child;
|
||||
};
|
||||
@@ -230,65 +213,7 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
container
|
||||
);
|
||||
refContainer.sketches.push(ref);
|
||||
//TODO: remove or move sort
|
||||
refContainer.sketches.sort((left, right) =>
|
||||
left.name.localeCompare(right.name)
|
||||
);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
// Built-ins are included inside the IDE.
|
||||
protected async load(path: string): Promise<SketchContainer> {
|
||||
if (!(await promisify(fs.exists)(path))) {
|
||||
throw new Error('Examples are not available');
|
||||
}
|
||||
const stat = await promisify(fs.stat)(path);
|
||||
if (!stat.isDirectory) {
|
||||
throw new Error(`${path} is not a directory.`);
|
||||
}
|
||||
const names = await promisify(fs.readdir)(path);
|
||||
const sketches: SketchRef[] = [];
|
||||
const children: SketchContainer[] = [];
|
||||
for (const p of names.map((name) => join(path, name))) {
|
||||
const stat = await promisify(fs.stat)(p);
|
||||
if (stat.isDirectory()) {
|
||||
const sketch = await this.tryLoadSketch(p);
|
||||
if (sketch) {
|
||||
sketches.push({ name: sketch.name, uri: sketch.uri });
|
||||
sketches.sort((left, right) => left.name.localeCompare(right.name));
|
||||
} else {
|
||||
const child = await this.load(p);
|
||||
children.push(child);
|
||||
children.sort((left, right) => left.label.localeCompare(right.label));
|
||||
}
|
||||
}
|
||||
}
|
||||
const label = basename(path);
|
||||
return {
|
||||
label,
|
||||
children,
|
||||
sketches,
|
||||
};
|
||||
}
|
||||
|
||||
protected async group(paths: string[]): Promise<Map<string, fs.Stats>> {
|
||||
const map = new Map<string, fs.Stats>();
|
||||
for (const path of paths) {
|
||||
const stat = await promisify(fs.stat)(path);
|
||||
map.set(path, stat);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
protected async tryLoadSketch(path: string): Promise<Sketch | undefined> {
|
||||
try {
|
||||
const sketch = await this.sketchesService.loadSketch(
|
||||
FileUri.create(path).toString()
|
||||
);
|
||||
return sketch;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
arduino-ide-extension/src/node/service-error.ts
Normal file
23
arduino-ide-extension/src/node/service-error.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Metadata, StatusObject } from '@grpc/grpc-js';
|
||||
|
||||
export type ServiceError = StatusObject & Error;
|
||||
export namespace ServiceError {
|
||||
export function is(arg: unknown): arg is ServiceError {
|
||||
return arg instanceof Error && isStatusObjet(arg);
|
||||
}
|
||||
function isStatusObjet(arg: unknown): arg is StatusObject {
|
||||
if (typeof arg === 'object') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const any = arg as any;
|
||||
return (
|
||||
!!arg &&
|
||||
'code' in arg &&
|
||||
'details' in arg &&
|
||||
typeof any.details === 'string' &&
|
||||
'metadata' in arg &&
|
||||
any.metadata instanceof Metadata
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
export class SimpleBuffer {
|
||||
private chunks: Uint8Array[] = [];
|
||||
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import { OutputMessage } from '../../common/protocol';
|
||||
|
||||
const DEFAULT_FLUS_TIMEOUT_MS = 32;
|
||||
|
||||
export class SimpleBuffer implements Disposable {
|
||||
private readonly flush: () => void;
|
||||
private readonly chunks = Chunks.create();
|
||||
private flushInterval?: NodeJS.Timeout;
|
||||
|
||||
private flush: () => void;
|
||||
|
||||
constructor(onFlush: (chunk: string) => void, flushTimeout: number) {
|
||||
const flush = () => {
|
||||
if (this.chunks.length > 0) {
|
||||
const chunkString = Buffer.concat(this.chunks).toString();
|
||||
this.clearChunks();
|
||||
|
||||
onFlush(chunkString);
|
||||
onFlush(chunks);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,12 +21,15 @@ export class SimpleBuffer {
|
||||
this.flushInterval = setInterval(flush, flushTimeout);
|
||||
}
|
||||
|
||||
public addChunk(chunk: Uint8Array): void {
|
||||
this.chunks.push(chunk);
|
||||
public addChunk(
|
||||
chunk: Uint8Array,
|
||||
severity: OutputMessage.Severity = OutputMessage.Severity.Info
|
||||
): void {
|
||||
this.chunks.get(severity)?.push(chunk);
|
||||
}
|
||||
|
||||
private clearChunks(): void {
|
||||
this.chunks = [];
|
||||
Chunks.clear(this.chunks);
|
||||
}
|
||||
|
||||
public clearFlushInterval(): void {
|
||||
@@ -32,6 +37,37 @@ export class SimpleBuffer {
|
||||
this.clearChunks();
|
||||
|
||||
clearInterval(this.flushInterval);
|
||||
this.clearChunks();
|
||||
this.flushInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
type Chunks = Map<OutputMessage.Severity, Uint8Array[]>;
|
||||
namespace Chunks {
|
||||
export function create(): Chunks {
|
||||
return new Map([
|
||||
[OutputMessage.Severity.Error, []],
|
||||
[OutputMessage.Severity.Warning, []],
|
||||
[OutputMessage.Severity.Info, []],
|
||||
]);
|
||||
}
|
||||
export function clear(chunks: Chunks): Chunks {
|
||||
for (const chunk of chunks.values()) {
|
||||
chunk.length = 0;
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
export function isEmpty(chunks: Chunks): boolean {
|
||||
return ![...chunks.values()].some((chunk) => Boolean(chunk.length));
|
||||
}
|
||||
export function toString(
|
||||
chunks: Chunks
|
||||
): Map<OutputMessage.Severity, string | undefined> {
|
||||
return new Map(
|
||||
Array.from(chunks.entries()).map(([severity, buffers]) => [
|
||||
severity,
|
||||
buffers.length ? Buffer.concat(buffers).toString() : undefined,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user