import { injectable, inject, named } from 'inversify'; import { ILogger } from '@theia/core/lib/common/logger'; import { notEmpty } from '@theia/core/lib/common/objects'; import { BoardsService, Installable, BoardsPackage, Board, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer, ResponseService, NotificationServiceServer, AvailablePorts, BoardWithPackage } from '../common/protocol'; import { PlatformInstallRequest, PlatformListRequest, PlatformListResponse, PlatformSearchRequest, PlatformSearchResponse, PlatformUninstallRequest } from './cli-protocol/cc/arduino/cli/commands/v1/core_pb'; import { Platform } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; import { BoardDiscovery } from './board-discovery'; import { CoreClientAware } from './core-client-provider'; import { BoardDetailsRequest, BoardDetailsResponse, BoardSearchRequest } from './cli-protocol/cc/arduino/cli/commands/v1/board_pb'; import { ListProgrammersAvailableForUploadRequest, ListProgrammersAvailableForUploadResponse } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; import { InstallWithProgress } from './grpc-installable'; @injectable() export class BoardsServiceImpl extends CoreClientAware implements BoardsService { @inject(ILogger) protected logger: ILogger; @inject(ILogger) @named('discovery') protected discoveryLogger: ILogger; @inject(ResponseService) protected readonly responseService: ResponseService; @inject(NotificationServiceServer) protected readonly notificationService: NotificationServiceServer; @inject(BoardDiscovery) protected readonly boardDiscovery: BoardDiscovery; async getState(): Promise { return this.boardDiscovery.state; } async getAttachedBoards(): Promise { return this.boardDiscovery.getAttachedBoards(); } async getAvailablePorts(): Promise { return this.boardDiscovery.getAvailablePorts(); } async getBoardDetails(options: { fqbn: string }): Promise { const coreClient = await this.coreClient(); const { client, instance } = coreClient; const { fqbn } = options; const detailsReq = new BoardDetailsRequest(); detailsReq.setInstance(instance); detailsReq.setFqbn(fqbn); const detailsResp = await new Promise((resolve, reject) => client.boardDetails(detailsReq, (err, resp) => { if (err) { // Required cores are not installed manually: https://github.com/arduino/arduino-cli/issues/954 if ((err.message.indexOf('missing platform release') !== -1 && err.message.indexOf('referenced by board') !== -1) // Platform is not installed. || err.message.indexOf('platform') !== -1 && err.message.indexOf('not installed') !== -1) { resolve(undefined); return; } // It's a hack to handle https://github.com/arduino/arduino-cli/issues/1262 gracefully. if (err.message.indexOf('unknown package') !== -1) { resolve(undefined); return; } reject(err); return; } resolve(resp); })); if (!detailsResp) { return undefined; } const debuggingSupported = detailsResp.getDebuggingSupported(); const requiredTools = detailsResp.getToolsDependenciesList().map(t => { name: t.getName(), packager: t.getPackager(), version: t.getVersion() }); const configOptions = detailsResp.getConfigOptionsList().map(c => { label: c.getOptionLabel(), option: c.getOption(), values: c.getValuesList().map(v => { value: v.getValue(), label: v.getValueLabel(), selected: v.getSelected() }) }); const listReq = new ListProgrammersAvailableForUploadRequest(); listReq.setInstance(instance); listReq.setFqbn(fqbn); const listResp = await new Promise((resolve, reject) => client.listProgrammersAvailableForUpload(listReq, (err, resp) => { if (err) { reject(err); return; } resolve(resp); })); const programmers = listResp.getProgrammersList().map(p => { id: p.getId(), name: p.getName(), platform: p.getPlatform() }); let VID = 'N/A'; let PID = 'N/A'; const usbId = detailsResp.getIdentificationPrefsList().map(item => item.getUsbId()).find(notEmpty); if (usbId) { VID = usbId.getVid(); PID = usbId.getPid(); } return { fqbn, requiredTools, configOptions, programmers, debuggingSupported, VID, PID }; } async getBoardPackage(options: { id: string }): Promise { const { id: expectedId } = options; if (!expectedId) { return undefined; } const packages = await this.search({ query: expectedId }); return packages.find(({ id }) => id === expectedId); } async getContainerBoardPackage(options: { fqbn: string }): Promise { const { fqbn: expectedFqbn } = options; if (!expectedFqbn) { return undefined; } const packages = await this.search({}); return packages.find(({ boards }) => boards.some(({ fqbn }) => fqbn === expectedFqbn)); } async searchBoards({ query }: { query?: string }): Promise { const { instance, client } = await this.coreClient(); const req = new BoardSearchRequest(); req.setSearchArgs(query || ''); req.setInstance(instance); const boards = await new Promise((resolve, reject) => { client.boardSearch(req, (error, resp) => { if (error) { reject(error); return; } const boards: Array = []; for (const board of resp.getBoardsList()) { const platform = board.getPlatform(); if (platform) { boards.push({ name: board.getName(), fqbn: board.getFqbn(), packageId: platform.getId(), packageName: platform.getName() }); } } resolve(boards); }) }); return boards; } async search(options: { query?: string }): Promise { const coreClient = await this.coreClient(); const { client, instance } = coreClient; const installedPlatformsReq = new PlatformListRequest(); installedPlatformsReq.setInstance(instance); const installedPlatformsResp = await new Promise((resolve, reject) => client.platformList(installedPlatformsReq, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)) ); const installedPlatforms = installedPlatformsResp.getInstalledPlatformsList(); const req = new PlatformSearchRequest(); req.setSearchArgs(options.query || ''); req.setAllVersions(true); req.setInstance(instance); const resp = await new Promise((resolve, reject) => client.platformSearch(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp))); const packages = new Map(); const toPackage = (platform: Platform) => { let installedVersion: string | undefined; const matchingPlatform = installedPlatforms.find(ip => ip.getId() === platform.getId()); if (!!matchingPlatform) { installedVersion = matchingPlatform.getInstalled(); } return { id: platform.getId(), name: platform.getName(), author: platform.getMaintainer(), availableVersions: [platform.getLatest()], description: platform.getBoardsList().map(b => b.getName()).join(', '), installable: true, deprecated: platform.getDeprecated(), summary: 'Boards included in this package:', installedVersion, boards: platform.getBoardsList().map(b => { name: b.getName(), fqbn: b.getFqbn() }), moreInfoLink: platform.getWebsite() } } // We must group the cores by ID, and sort platforms by, first the installed version, then version alphabetical order. // Otherwise we lose the FQBN information. const groupedById: Map = new Map(); for (const platform of resp.getSearchOutputList()) { const id = platform.getId(); if (groupedById.has(id)) { groupedById.get(id)!.push(platform); } else { groupedById.set(id, [platform]); } } const installedAwareVersionComparator = (left: Platform, right: Platform) => { // XXX: we cannot rely on `platform.getInstalled()`, it is always an empty string. const leftInstalled = !!installedPlatforms.find(ip => ip.getId() === left.getId() && ip.getInstalled() === left.getLatest()); const rightInstalled = !!installedPlatforms.find(ip => ip.getId() === right.getId() && ip.getInstalled() === right.getLatest()); if (leftInstalled && !rightInstalled) { return -1; } if (!leftInstalled && rightInstalled) { return 1; } return Installable.Version.COMPARATOR(left.getLatest(), right.getLatest()); // Higher version comes first. } for (const id of groupedById.keys()) { groupedById.get(id)!.sort(installedAwareVersionComparator); } for (const id of groupedById.keys()) { for (const platform of groupedById.get(id)!) { const id = platform.getId(); const pkg = packages.get(id); if (pkg) { pkg.availableVersions.push(platform.getLatest()); pkg.availableVersions.sort(Installable.Version.COMPARATOR).reverse(); } else { packages.set(id, toPackage(platform)); } } } return [...packages.values()]; } async install(options: { item: BoardsPackage, progressId?: string, version?: Installable.Version }): Promise { const item = options.item; const version = !!options.version ? options.version : item.availableVersions[0]; const coreClient = await this.coreClient(); const { client, instance } = coreClient; const [platform, architecture] = item.id.split(':'); const req = new PlatformInstallRequest(); req.setInstance(instance); req.setArchitecture(architecture); req.setPlatformPackage(platform); req.setVersion(version); console.info('>>> Starting boards package installation...', item); const resp = client.platformInstall(req); resp.on('data', InstallWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService })); await new Promise((resolve, reject) => { resp.on('end', resolve); resp.on('error', error => { this.responseService.appendToOutput({ chunk: `Failed to install platform: ${item.id}.\n` }); this.responseService.appendToOutput({ chunk: error.toString() }); reject(error); }); }); const items = await this.search({}); const updated = items.find(other => BoardsPackage.equals(other, item)) || item; this.notificationService.notifyPlatformInstalled({ item: updated }); console.info('<<< Boards package installation done.', item); } async uninstall(options: { item: BoardsPackage, progressId?: string }): Promise { const { item, progressId } = options; const coreClient = await this.coreClient(); const { client, instance } = coreClient; const [platform, architecture] = item.id.split(':'); const req = new PlatformUninstallRequest(); req.setInstance(instance); req.setArchitecture(architecture); req.setPlatformPackage(platform); console.info('>>> Starting boards package uninstallation...', item); const resp = client.platformUninstall(req); resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService })); await new Promise((resolve, reject) => { resp.on('end', resolve); resp.on('error', reject); }); // Here, unlike at `install` we send out the argument `item`. Otherwise, we would not know about the board FQBN. this.notificationService.notifyPlatformUninstalled({ item }); console.info('<<< Boards package uninstallation done.', item); } }