arduino-ide/arduino-ide-extension/src/node/boards-service-impl.ts
Francesco Stasi 4fa2024266 [atl-1280] Board packages hints
handle cornercase when 2 packages are associated to the same board

updated cli version and grpc

support deprecated cores in the boards manager

bump cli version

Bump ArduinoCLI version to latest release

Add package version in notification
2021-05-11 14:07:54 +02:00

322 lines
13 KiB
TypeScript

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<AvailablePorts> {
return this.boardDiscovery.state;
}
async getAttachedBoards(): Promise<Board[]> {
return this.boardDiscovery.getAttachedBoards();
}
async getAvailablePorts(): Promise<Port[]> {
return this.boardDiscovery.getAvailablePorts();
}
async getBoardDetails(options: { fqbn: string }): Promise<BoardDetails | undefined> {
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<BoardDetailsResponse | undefined>((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 => <Tool>{
name: t.getName(),
packager: t.getPackager(),
version: t.getVersion()
});
const configOptions = detailsResp.getConfigOptionsList().map(c => <ConfigOption>{
label: c.getOptionLabel(),
option: c.getOption(),
values: c.getValuesList().map(v => <ConfigValue>{
value: v.getValue(),
label: v.getValueLabel(),
selected: v.getSelected()
})
});
const listReq = new ListProgrammersAvailableForUploadRequest();
listReq.setInstance(instance);
listReq.setFqbn(fqbn);
const listResp = await new Promise<ListProgrammersAvailableForUploadResponse>((resolve, reject) => client.listProgrammersAvailableForUpload(listReq, (err, resp) => {
if (err) {
reject(err);
return;
}
resolve(resp);
}));
const programmers = listResp.getProgrammersList().map(p => <Programmer>{
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<BoardsPackage | undefined> {
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<BoardsPackage | undefined> {
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<BoardWithPackage[]> {
const { instance, client } = await this.coreClient();
const req = new BoardSearchRequest();
req.setSearchArgs(query || '');
req.setInstance(instance);
const boards = await new Promise<BoardWithPackage[]>((resolve, reject) => {
client.boardSearch(req, (error, resp) => {
if (error) {
reject(error);
return;
}
const boards: Array<BoardWithPackage> = [];
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<BoardsPackage[]> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const installedPlatformsReq = new PlatformListRequest();
installedPlatformsReq.setInstance(instance);
const installedPlatformsResp = await new Promise<PlatformListResponse>((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<PlatformSearchResponse>((resolve, reject) => client.platformSearch(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)));
const packages = new Map<string, BoardsPackage>();
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 => <Board>{ 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<string, Platform[]> = 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<void> {
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<void>((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<void> {
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<void>((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);
}
}