import { FileUri } from '@theia/core/lib/node/file-uri'; import { inject, injectable } from '@theia/core/shared/inversify'; 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, CoreError, } from '../common/protocol/core-service'; import { CompileRequest, CompileResponse, } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb'; import { CoreClientAware } from './core-client-provider'; import { BurnBootloaderRequest, BurnBootloaderResponse, UploadRequest, UploadResponse, UploadUsingProgrammerRequest, UploadUsingProgrammerResponse, } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; import { ResponseService } from '../common/protocol/response-service'; import { Board, OutputMessage, Port, Status } from '../common/protocol'; import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb'; import { Port as GrpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb'; import { ApplicationError, CommandService, Disposable, nls } from '@theia/core'; import { MonitorManager } from './monitor-manager'; import { AutoFlushingBuffer } from './utils/buffers'; 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'; @injectable() export class CoreServiceImpl extends CoreClientAware implements CoreService { @inject(ResponseService) private readonly responseService: ResponseService; @inject(MonitorManager) private readonly monitorManager: MonitorManager; @inject(CommandService) private readonly commandService: CommandService; async compile( options: CoreService.Compile.Options & { exportBinaries?: boolean; compilerWarnings?: CompilerWarnings; } ): Promise { const coreClient = await this.coreClient; const { client, instance } = coreClient; let buildPath: string | undefined = undefined; const handler = this.createOnDataHandler((response) => { const currentBuildPath = response.getBuildPath(); if (!buildPath && currentBuildPath) { buildPath = currentBuildPath; } else { if (!!currentBuildPath && currentBuildPath !== buildPath) { throw new Error( `The CLI has already provided a build path: <${buildPath}>, and there is a new build path value: <${currentBuildPath}>.` ); } } }); const request = this.compileRequest(options, instance); return new Promise((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(); if (!buildPath) { console.error( `Have not received the build path from the CLI while running the compilation.` ); } else { this.fireBuildDidComplete(FileUri.create(buildPath).toString()); } }); } // This executes on the frontend, the VS Code extension receives it, and sends an `ino/buildDidComplete` notification to the language server. private fireBuildDidComplete(buildOutputUri: string): void { const params = { buildOutputUri, }; console.info( `Executing 'arduino.languageserver.notifyBuildDidComplete' with ${JSON.stringify( params )}` ); this.commandService .executeCommand('arduino.languageserver.notifyBuildDidComplete', params) .catch((err) => console.error( `Unexpected error when firing event on build did complete. ${buildOutputUri}`, err ) ); } 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) { request.setFqbn(board.fqbn); } if (compilerWarnings) { request.setWarnings(compilerWarnings.toLowerCase()); } 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); request.setExportBinaries(exportBinaries); } this.mergeSourceOverrides(request, options); return request; } upload(options: CoreService.Upload.Options): Promise { return this.doUpload( options, () => new UploadRequest(), (client, req) => client.upload(req), (message: string, locations: CoreError.ErrorLocation[]) => CoreError.UploadFailed(message, locations), 'upload' ); } async uploadUsingProgrammer( options: CoreService.Upload.Options ): Promise { return this.doUpload( options, () => new UploadUsingProgrammerRequest(), (client, req) => client.uploadUsingProgrammer(req), (message: string, locations: CoreError.ErrorLocation[]) => CoreError.UploadUsingProgrammerFailed(message, locations), 'upload using programmer' ); } protected async doUpload( options: CoreService.Upload.Options, requestFactory: () => UploadRequest | UploadUsingProgrammerRequest, responseHandler: ( client: ArduinoCoreServiceClient, request: UploadRequest | UploadUsingProgrammerRequest ) => ClientReadableStream, errorHandler: ( message: string, locations: CoreError.ErrorLocation[] ) => ApplicationError, task: string ): Promise { await this.compile(Object.assign(options, { exportBinaries: false })); 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((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); }) ); } 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) { request.setFqbn(board.fqbn); } request.setPort(this.createPort(port)); if (programmer) { request.setProgrammer(programmer.id); } request.setVerbose(options.verbose); request.setVerify(options.verify); options.userFields.forEach((e) => { request.getUserFieldsMap().set(e.name, e.value); }); return request; } async burnBootloader(options: CoreService.Bootloader.Options): Promise { const coreClient = await this.coreClient; const { client, instance } = coreClient; const handler = this.createOnDataHandler(); const request = this.burnBootloaderRequest(options, instance); return this.notifyUploadWillStart(options).then(() => new Promise((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); }) ); } 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( onResponse?: (response: R) => void ): Disposable & { stderr: Buffer[]; onData: (response: R) => void; } { const stderr: Buffer[] = []; const buffer = new AutoFlushingBuffer((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); }, onResponse ); 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 { return this.monitorManager.notifyUploadStarted(board, port); } private async notifyUploadDidFinish({ board, port, }: { board?: Board | undefined; port?: Port | undefined; }): Promise { return this.monitorManager.notifyUploadFinished(board, port); } private mergeSourceOverrides( req: { getSourceOverrideMap(): jspb.Map }, options: CoreService.Compile.Options ): void { const sketchPath = FileUri.fsPath(options.sketch.uri); for (const uri of Object.keys(options.sourceOverride)) { const content = options.sourceOverride[uri]; if (content) { const relativePath = relative(sketchPath, FileUri.fsPath(uri)); req.getSourceOverrideMap().set(relativePath, content); } } } 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( stderr: Uint8Array[], onData: (out: Uint8Array, err: Uint8Array) => void, onResponse?: (response: R) => void ): (response: R) => void { return (response: R) => { const out = response.getOutStream_asU8(); const err = response.getErrStream_asU8(); stderr.push(err); onData(out, err); if (onResponse) { onResponse(response); } }; } }