#854 fix platform installation only offered if port is selected (#1130)

* ensure desired prompts shown + refactor

* pr review changes
This commit is contained in:
David Simpson 2022-07-06 08:38:51 +02:00 committed by GitHub
parent 0ce065e496
commit 7f2b849963
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 257 additions and 95 deletions

View File

@ -5,19 +5,37 @@ import {
BoardsService, BoardsService,
BoardsPackage, BoardsPackage,
Board, Board,
Port,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import { BoardsServiceProvider } from './boards-service-provider'; import { BoardsServiceProvider } from './boards-service-provider';
import { BoardsConfig } from './boards-config';
import { Installable, ResponseServiceArduino } from '../../common/protocol'; import { Installable, ResponseServiceArduino } from '../../common/protocol';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution'; import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { NotificationCenter } from '../notification-center';
interface AutoInstallPromptAction {
// isAcceptance, whether or not the action indicates acceptance of auto-install proposal
isAcceptance?: boolean;
key: string;
handler: (...args: unknown[]) => unknown;
}
type AutoInstallPromptActions = AutoInstallPromptAction[];
/** /**
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not * Listens on `BoardsConfig.Config` changes, if a board is selected which does not
* have the corresponding core installed, it proposes the user to install the core. * have the corresponding core installed, it proposes the user to install the core.
*/ */
// * Cases in which we do not show the auto-install prompt:
// 1. When a related platform is already installed
// 2. When a prompt is already showing in the UI
// 3. When a board is unplugged
@injectable() @injectable()
export class BoardsAutoInstaller implements FrontendApplicationContribution { export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(MessageService) @inject(MessageService)
protected readonly messageService: MessageService; protected readonly messageService: MessageService;
@ -36,22 +54,106 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
// Workaround for https://github.com/eclipse-theia/theia/issues/9349 // Workaround for https://github.com/eclipse-theia/theia/issues/9349
protected notifications: Board[] = []; protected notifications: Board[] = [];
// * "refusal" meaning a "prompt action" not accepting the auto-install offer ("X" or "install manually")
// we can use "portSelectedOnLastRefusal" to deduce when a board is unplugged after a user has "refused"
// an auto-install prompt. Important to know as we do not want "an unplug" to trigger a "refused" prompt
// showing again
private portSelectedOnLastRefusal: Port | undefined;
private lastRefusedPackageId: string | undefined;
onStart(): void { onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged( const setEventListeners = () => {
this.ensureCoreExists.bind(this) this.boardsServiceClient.onBoardsConfigChanged((config) => {
); const { selectedBoard, selectedPort } = config;
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
const boardWasUnplugged =
!selectedPort && this.portSelectedOnLastRefusal;
this.clearLastRefusedPromptInfo();
if (
boardWasUnplugged ||
!selectedBoard ||
this.promptAlreadyShowingForBoard(selectedBoard)
) {
return;
} }
protected ensureCoreExists(config: BoardsConfig.Config): void { this.ensureCoreExists(selectedBoard, selectedPort);
const { selectedBoard, selectedPort } = config; });
if (
selectedBoard && // we "clearRefusedPackageInfo" if a "refused" package is eventually
selectedPort && // installed, though this is not strictly necessary. It's more of a
!this.notifications.find((board) => Board.sameAs(board, selectedBoard)) // cleanup, to ensure the related variables are representative of
) { // current state.
this.notificationCenter.onPlatformInstalled((installed) => {
if (this.lastRefusedPackageId === installed.item.id) {
this.clearLastRefusedPromptInfo();
}
});
};
// we should invoke this.ensureCoreExists only once we're sure
// everything has been reconciled
this.boardsServiceClient.reconciled.then(() => {
const { selectedBoard, selectedPort } =
this.boardsServiceClient.boardsConfig;
if (selectedBoard) {
this.ensureCoreExists(selectedBoard, selectedPort);
}
setEventListeners();
});
}
private removeNotificationByBoard(selectedBoard: Board): void {
const index = this.notifications.findIndex((notification) =>
Board.sameAs(notification, selectedBoard)
);
if (index !== -1) {
this.notifications.splice(index, 1);
}
}
private clearLastRefusedPromptInfo(): void {
this.lastRefusedPackageId = undefined;
this.portSelectedOnLastRefusal = undefined;
}
private setLastRefusedPromptInfo(
packageId: string,
selectedPort?: Port
): void {
this.lastRefusedPackageId = packageId;
this.portSelectedOnLastRefusal = selectedPort;
}
private promptAlreadyShowingForBoard(board: Board): boolean {
return Boolean(
this.notifications.find((notification) =>
Board.sameAs(notification, board)
)
);
}
protected ensureCoreExists(selectedBoard: Board, selectedPort?: Port): void {
this.notifications.push(selectedBoard); this.notifications.push(selectedBoard);
this.boardsService.search({}).then((packages) => { this.boardsService.search({}).then((packages) => {
const candidate = this.getInstallCandidate(packages, selectedBoard);
if (candidate) {
this.showAutoInstallPrompt(candidate, selectedBoard, selectedPort);
} else {
this.removeNotificationByBoard(selectedBoard);
}
});
}
private getInstallCandidate(
packages: BoardsPackage[],
selectedBoard: Board
): BoardsPackage | undefined {
// filter packagesForBoard selecting matches from the cli (installed packages) // filter packagesForBoard selecting matches from the cli (installed packages)
// and matches based on the board name // and matches based on the board name
// NOTE: this ensures the Deprecated & new packages are all in the array // NOTE: this ensures the Deprecated & new packages are all in the array
@ -63,9 +165,7 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
); );
// check if one of the packages for the board is already installed. if so, no hint // check if one of the packages for the board is already installed. if so, no hint
if ( if (packagesForBoard.some(({ installedVersion }) => !!installedVersion)) {
packagesForBoard.some(({ installedVersion }) => !!installedVersion)
) {
return; return;
} }
@ -73,60 +173,109 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
// CLI returns the packages already sorted with the deprecated ones at the end of the list // CLI returns the packages already sorted with the deprecated ones at the end of the list
// in order to ensure the new ones are preferred // in order to ensure the new ones are preferred
const candidates = packagesForBoard.filter( const candidates = packagesForBoard.filter(
({ installable, installedVersion }) => ({ installable, installedVersion }) => installable && !installedVersion
installable && !installedVersion
); );
const candidate = candidates[0]; return candidates[0];
if (candidate) { }
private showAutoInstallPrompt(
candidate: BoardsPackage,
selectedBoard: Board,
selectedPort?: Port
): void {
const candidateName = candidate.name;
const version = candidate.availableVersions[0] const version = candidate.availableVersions[0]
? `[v ${candidate.availableVersions[0]}]` ? `[v ${candidate.availableVersions[0]}]`
: ''; : '';
const info = this.generatePromptInfoText(
candidateName,
version,
selectedBoard.name
);
const actions = this.createPromptActions(candidate);
const onRefuse = () => {
this.setLastRefusedPromptInfo(candidate.id, selectedPort);
};
const handleAction = this.createOnAnswerHandler(actions, onRefuse);
const onAnswer = (answer: string) => {
this.removeNotificationByBoard(selectedBoard);
handleAction(answer);
};
this.messageService
.info(info, ...actions.map((action) => action.key))
.then(onAnswer);
}
private generatePromptInfoText(
candidateName: string,
version: string,
boardName: string
): string {
return 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?',
candidateName,
version,
boardName
);
}
private createPromptActions(
candidate: BoardsPackage
): AutoInstallPromptActions {
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes'); const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const manualInstall = nls.localize( const manualInstall = nls.localize(
'arduino/board/installManually', 'arduino/board/installManually',
'Install Manually' 'Install Manually'
); );
// tslint:disable-next-line:max-line-length
this.messageService const actions: AutoInstallPromptActions = [
.info( {
nls.localize( isAcceptance: true,
'arduino/board/installNow', key: yes,
'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?', handler: () => {
candidate.name, return Installable.installWithProgress({
version,
selectedBoard.name
),
manualInstall,
yes
)
.then(async (answer) => {
const index = this.notifications.findIndex((board) =>
Board.sameAs(board, selectedBoard)
);
if (index !== -1) {
this.notifications.splice(index, 1);
}
if (answer === yes) {
await Installable.installWithProgress({
installable: this.boardsService, installable: this.boardsService,
item: candidate, item: candidate,
messageService: this.messageService, messageService: this.messageService,
responseService: this.responseService, responseService: this.responseService,
version: candidate.availableVersions[0], version: candidate.availableVersions[0],
}); });
return; },
} },
if (answer === manualInstall) { {
key: manualInstall,
handler: () => {
this.boardsManagerFrontendContribution this.boardsManagerFrontendContribution
.openView({ reveal: true }) .openView({ reveal: true })
.then((widget) => .then((widget) =>
widget.refresh(candidate.name.toLocaleLowerCase()) widget.refresh(candidate.name.toLocaleLowerCase())
); );
},
},
];
return actions;
} }
});
} private createOnAnswerHandler(
}); actions: AutoInstallPromptActions,
onRefuse?: () => void
): (answer: string) => void {
return (answer) => {
const actionToHandle = actions.find((action) => action.key === answer);
actionToHandle?.handler();
if (!actionToHandle?.isAcceptance && onRefuse) {
onRefuse();
} }
};
} }
} }

View File

@ -20,6 +20,7 @@ import { NotificationCenter } from '../notification-center';
import { ArduinoCommands } from '../arduino-commands'; import { ArduinoCommands } from '../arduino-commands';
import { StorageWrapper } from '../storage-wrapper'; import { StorageWrapper } from '../storage-wrapper';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { Deferred } from '@theia/core/lib/common/promise-util';
@injectable() @injectable()
export class BoardsServiceProvider implements FrontendApplicationContribution { export class BoardsServiceProvider implements FrontendApplicationContribution {
@ -73,6 +74,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
this.onAvailableBoardsChangedEmitter.event; this.onAvailableBoardsChangedEmitter.event;
readonly onAvailablePortsChanged = this.onAvailablePortsChangedEmitter.event; readonly onAvailablePortsChanged = this.onAvailablePortsChangedEmitter.event;
private readonly _reconciled = new Deferred<void>();
onStart(): void { onStart(): void {
this.notificationCenter.onAttachedBoardsChanged( this.notificationCenter.onAttachedBoardsChanged(
this.notifyAttachedBoardsChanged.bind(this) this.notifyAttachedBoardsChanged.bind(this)
@ -88,14 +91,22 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
this.boardsService.getAttachedBoards(), this.boardsService.getAttachedBoards(),
this.boardsService.getAvailablePorts(), this.boardsService.getAvailablePorts(),
this.loadState(), this.loadState(),
]).then(([attachedBoards, availablePorts]) => { ]).then(async ([attachedBoards, availablePorts]) => {
this._attachedBoards = attachedBoards; this._attachedBoards = attachedBoards;
this._availablePorts = availablePorts; this._availablePorts = availablePorts;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts); this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
this.reconcileAvailableBoards().then(() => this.tryReconnect());
await this.reconcileAvailableBoards();
this.tryReconnect();
this._reconciled.resolve();
}); });
} }
get reconciled(): Promise<void> {
return this._reconciled.promise;
}
protected notifyAttachedBoardsChanged( protected notifyAttachedBoardsChanged(
event: AttachedBoardsChangeEvent event: AttachedBoardsChangeEvent
): void { ): void {
@ -209,7 +220,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
} }
} }
protected async tryReconnect(): Promise<boolean> { protected tryReconnect(): boolean {
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) { if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
for (const board of this.availableBoards.filter( for (const board of this.availableBoards.filter(
({ state }) => state !== AvailableBoard.State.incomplete ({ state }) => state !== AvailableBoard.State.incomplete
@ -231,7 +242,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
if ( if (
this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn && this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn &&
this.latestValidBoardsConfig.selectedBoard.name === board.name && this.latestValidBoardsConfig.selectedBoard.name === board.name &&
this.latestValidBoardsConfig.selectedPort.protocol === board.port?.protocol this.latestValidBoardsConfig.selectedPort.protocol ===
board.port?.protocol
) { ) {
this.boardsConfig = { this.boardsConfig = {
...this.latestValidBoardsConfig, ...this.latestValidBoardsConfig,
@ -534,7 +546,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
protected getLastSelectedBoardOnPortKey(port: Port | string): string { protected getLastSelectedBoardOnPortKey(port: Port | string): string {
// TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`. // TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`.
return `last-selected-board-on-port:${typeof port === 'string' ? port : port.address return `last-selected-board-on-port:${
typeof port === 'string' ? port : port.address
}`; }`;
} }