import { injectable, inject, named } from 'inversify'; import { ILogger } from '@theia/core/lib/common/logger'; import { BoardsService, Installable, BoardsPackage, Board, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer, OutputService, NotificationServiceServer, AvailablePorts, BoardWithPackage } from '../common/protocol'; import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp, Platform, PlatformUninstallResp, PlatformUninstallReq } from './cli-protocol/commands/core_pb'; import { BoardDiscovery } from './board-discovery'; import { CoreClientProvider } from './core-client-provider'; import { BoardDetailsReq, BoardDetailsResp } from './cli-protocol/commands/board_pb'; import { ListProgrammersAvailableForUploadReq, ListProgrammersAvailableForUploadResp } from './cli-protocol/commands/upload_pb'; @injectable() export class BoardsServiceImpl implements BoardsService { @inject(ILogger) protected logger: ILogger; @inject(ILogger) @named('discovery') protected discoveryLogger: ILogger; @inject(CoreClientProvider) protected readonly coreClientProvider: CoreClientProvider; @inject(OutputService) protected readonly outputService: OutputService; @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(); } private async coreClient(): Promise { const coreClient = await new Promise(async resolve => { const client = await this.coreClientProvider.client(); if (client) { resolve(client); return; } const toDispose = this.coreClientProvider.onClientReady(async () => { const client = await this.coreClientProvider.client(); if (client) { toDispose.dispose(); resolve(client); return; } }); }); return coreClient; } async getBoardDetails(options: { fqbn: string }): Promise { const coreClient = await this.coreClient(); const { client, instance } = coreClient; const { fqbn } = options; const detailsReq = new BoardDetailsReq(); 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; } 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 ListProgrammersAvailableForUploadReq(); 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() }); return { fqbn, requiredTools, configOptions, programmers, debuggingSupported }; } 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 allBoards(options: {}): Promise> { const results = await this.search(options); return results.map(item => item.boards.map(board => ({ ...board, packageName: item.name, packageId: item.id }))) .reduce((acc, curr) => acc.concat(curr), []); } async search(options: { query?: string }): Promise { const coreClient = await this.coreClient(); const { client, instance } = coreClient; const installedPlatformsReq = new PlatformListReq(); installedPlatformsReq.setInstance(instance); const installedPlatformsResp = await new Promise((resolve, reject) => client.platformList(installedPlatformsReq, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)) ); const installedPlatforms = installedPlatformsResp.getInstalledPlatformList(); const req = new PlatformSearchReq(); 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, 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, 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 PlatformInstallReq(); 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', (r: PlatformInstallResp) => { const prog = r.getProgress(); if (prog && prog.getFile()) { this.outputService.append({ name: 'board download', chunk: `downloading ${prog.getFile()}\n` }); } }); await new Promise((resolve, reject) => { resp.on('end', resolve); resp.on('error', reject); }); 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 }): Promise { const item = options.item; const coreClient = await this.coreClient(); const { client, instance } = coreClient; const [platform, architecture] = item.id.split(':'); const req = new PlatformUninstallReq(); req.setInstance(instance); req.setArchitecture(architecture); req.setPlatformPackage(platform); console.info('>>> Starting boards package uninstallation...', item); let logged = false; const resp = client.platformUninstall(req); resp.on('data', (_: PlatformUninstallResp) => { if (!logged) { this.outputService.append({ name: 'board uninstall', chunk: `uninstalling ${item.id}\n` }); logged = true; } }) 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); } }