import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageType } from '@theia/core/lib/common/message-service-protocol'; import { nls } from '@theia/core/lib/common/nls'; import { notEmpty } from '@theia/core/lib/common/objects'; import { inject, injectable } from '@theia/core/shared/inversify'; import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager'; import { InstallManually } from '../../common/nls'; import { Installable, ResponseServiceClient } from '../../common/protocol'; import { BoardIdentifier, BoardsPackage, BoardsService, createPlatformIdentifier, isBoardIdentifierChangeEvent, PlatformIdentifier, platformIdentifierEquals, serializePlatformIdentifier, } from '../../common/protocol/boards-service'; import { NotificationCenter } from '../notification-center'; import { BoardsServiceProvider } from './boards-service-provider'; import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution'; /** * Listens on `BoardsConfigChangeEvent`s, if a board is selected which does not * have the corresponding core installed, it proposes the user to install the core. */ @injectable() export class BoardsAutoInstaller implements FrontendApplicationContribution { @inject(NotificationCenter) private readonly notificationCenter: NotificationCenter; @inject(MessageService) private readonly messageService: MessageService; @inject(NotificationManager) private readonly notificationManager: NotificationManager; @inject(BoardsService) private readonly boardsService: BoardsService; @inject(BoardsServiceProvider) private readonly boardsServiceProvider: BoardsServiceProvider; @inject(ResponseServiceClient) private readonly responseService: ResponseServiceClient; @inject(BoardsListWidgetFrontendContribution) private readonly boardsManagerWidgetContribution: BoardsListWidgetFrontendContribution; // Workaround for https://github.com/eclipse-theia/theia/issues/9349 private readonly installNotificationInfos: Readonly<{ boardName: string; platformId: string; notificationId: string; }>[] = []; private readonly toDispose = new DisposableCollection(); onStart(): void { this.toDispose.pushAll([ this.boardsServiceProvider.onBoardsConfigDidChange((event) => { if (isBoardIdentifierChangeEvent(event)) { this.ensureCoreExists(event.selectedBoard); } }), this.notificationCenter.onPlatformDidInstall((event) => this.clearAllNotificationForPlatform(event.item.id) ), ]); this.boardsServiceProvider.ready.then(() => { const { selectedBoard } = this.boardsServiceProvider.boardsConfig; this.ensureCoreExists(selectedBoard); }); } private async findPlatformToInstall( selectedBoard: BoardIdentifier ): Promise { const platformId = await this.findPlatformIdToInstall(selectedBoard); if (!platformId) { return undefined; } const id = serializePlatformIdentifier(platformId); const platform = await this.boardsService.getBoardPackage({ id }); if (!platform) { console.warn(`Could not resolve platform for ID: ${id}`); return undefined; } if (platform.installedVersion) { return undefined; } return platform; } private async findPlatformIdToInstall( selectedBoard: BoardIdentifier ): Promise { const selectedBoardPlatformId = createPlatformIdentifier(selectedBoard); // The board is installed or the FQBN is available from the `board list watch` for Arduino boards. The latter might change! if (selectedBoardPlatformId) { const installedPlatforms = await this.boardsService.getInstalledPlatforms(); const installedPlatformIds = installedPlatforms .map((platform) => createPlatformIdentifier(platform.id)) .filter(notEmpty); if ( installedPlatformIds.every( (installedPlatformId) => !platformIdentifierEquals( installedPlatformId, selectedBoardPlatformId ) ) ) { return selectedBoardPlatformId; } } else { // IDE2 knows that selected board is not installed. Look for board `name` match in not yet installed platforms. // The order should be correct when there is a board name collision (e.g. Arduino Nano RP2040 from Arduino Mbed OS Nano Boards, [DEPRECATED] Arduino Mbed OS Nano Boards). The CLI boosts the platforms, so picking the first name match should be fine. const platforms = await this.boardsService.search({}); for (const platform of platforms) { // Ignore installed platforms if (platform.installedVersion) { continue; } if ( platform.boards.some((board) => board.name === selectedBoard.name) ) { const platformId = createPlatformIdentifier(platform.id); if (platformId) { return platformId; } } } } return undefined; } private async ensureCoreExists( selectedBoard: BoardIdentifier | undefined ): Promise { if (!selectedBoard) { return; } const candidate = await this.findPlatformToInstall(selectedBoard); if (!candidate) { return; } const platformIdToInstall = candidate.id; const selectedBoardName = selectedBoard.name; if ( this.installNotificationInfos.some( ({ boardName, platformId }) => platformIdToInstall === platformId && selectedBoardName === boardName ) ) { // Already has a notification for the board with the same platform. Nothing to do. return; } this.clearAllNotificationForPlatform(platformIdToInstall); const version = candidate.availableVersions[0] ? `[v ${candidate.availableVersions[0]}]` : ''; const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes'); const message = nls.localize( 'arduino/board/installNow', 'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?', candidate.name, version, selectedBoard.name ); const notificationId = this.notificationId(message, InstallManually, yes); this.installNotificationInfos.push({ boardName: selectedBoardName, platformId: platformIdToInstall, notificationId, }); const answer = await this.messageService.info( message, InstallManually, yes ); if (answer) { const index = this.installNotificationInfos.findIndex( ({ boardName, platformId }) => platformIdToInstall === platformId && selectedBoardName === boardName ); if (index !== -1) { this.installNotificationInfos.splice(index, 1); } if (answer === yes) { await Installable.installWithProgress({ installable: this.boardsService, item: candidate, messageService: this.messageService, responseService: this.responseService, version: candidate.availableVersions[0], }); return; } if (answer === InstallManually) { this.boardsManagerWidgetContribution .openView({ reveal: true }) .then((widget) => widget.refresh({ query: candidate.name.toLocaleLowerCase(), type: 'All', }) ); } } } private clearAllNotificationForPlatform(predicatePlatformId: string): void { // Discard all install notifications for the same platform. const notificationsLength = this.installNotificationInfos.length; for (let i = notificationsLength - 1; i >= 0; i--) { const { notificationId, platformId } = this.installNotificationInfos[i]; if (platformId === predicatePlatformId) { this.installNotificationInfos.splice(i, 1); this.notificationManager.clear(notificationId); } } } private notificationId(message: string, ...actions: string[]): string { return this.notificationManager['getMessageId']({ text: message, actions, type: MessageType.Info, }); } }