diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index b653f452..85c733bc 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -14,6 +14,8 @@ import { WorkspaceServiceExt } from './workspace-service-ext'; import { ToolOutputServiceClient } from '../common/protocol/tool-output-service'; import { ConfirmDialog } from '@theia/core/lib/browser'; import { QuickPickService } from '@theia/core/lib/common/quick-pick-service'; +import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution'; +import { BoardsNotificationService } from './boards-notification-service'; @injectable() @@ -37,6 +39,12 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr @inject(QuickPickService) protected readonly quickPickService: QuickPickService; + @inject(BoardsListWidgetFrontendContribution) + protected readonly boardsListWidgetFrontendContribution: BoardsListWidgetFrontendContribution; + + @inject(BoardsNotificationService) + protected readonly boardsNotificationService: BoardsNotificationService; + @postConstruct() protected async init(): Promise { // This is a hack. Otherwise, the backend services won't bind. @@ -60,7 +68,12 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr }); registry.registerItem({ id: ConnectedBoards.TOOLBAR_ID, - render: () => , + render: () => , isVisible: widget => this.isArduinoEditor(widget) }) } @@ -103,6 +116,25 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr }); } + private async onNoBoardsInstalled() { + const action = await this.messageService.info("You have no boards installed. Use the boards mangager to install one.", "Open Boards Manager"); + if (!action) { + return; + } + + this.boardsListWidgetFrontendContribution.openView({reveal: true}); + } + + private async onUnknownBoard() { + const action = await this.messageService.warn("There's a board connected for which you need to install software." + + " If this were not a PoC we would offer you the right package now.", "Open Boards Manager"); + if (!action) { + return; + } + + this.boardsListWidgetFrontendContribution.openView({reveal: true}); + } + private isArduinoEditor(maybeEditorWidget: any): boolean { if (maybeEditorWidget instanceof EditorWidget) { return maybeEditorWidget.editor.uri.toString().endsWith('.ino'); diff --git a/arduino-ide-extension/src/browser/arduino-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-frontend-module.ts index 5ce6fa32..97688bd2 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-frontend-module.ts @@ -22,6 +22,7 @@ import { ToolOutputServiceClient } from '../common/protocol/tool-output-service' import '../../src/browser/style/index.css'; import { ToolOutputService } from '../common/protocol/tool-output-service'; import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl'; +import { BoardsNotificationService } from './boards-notification-service'; export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => { // Commands and toolbar items @@ -44,6 +45,10 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un })); bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution); + // Boards Notification service for updating boards list + // TODO (post-PoC): move this to boards service/backend + bind(BoardsNotificationService).toSelf().inSingletonScope(); + // Boards service bind(BoardsService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath)).inSingletonScope(); diff --git a/arduino-ide-extension/src/browser/boards-notification-service.ts b/arduino-ide-extension/src/browser/boards-notification-service.ts new file mode 100644 index 00000000..0d529685 --- /dev/null +++ b/arduino-ide-extension/src/browser/boards-notification-service.ts @@ -0,0 +1,19 @@ +import { EventEmitter } from "events"; +import { injectable } from "inversify"; + +// TODO (post-PoC): move this to the backend / BoardsService +@injectable() +export class BoardsNotificationService { + + protected readonly emitter = new EventEmitter(); + + public on(event: 'boards-installed', listener: (...args: any[]) => void): this { + this.emitter.on(event, listener); + return this; + } + + public notifyBoardsInstalled() { + this.emitter.emit('boards-installed'); + } + +} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/boards/list-widget.tsx b/arduino-ide-extension/src/browser/boards/list-widget.tsx index 46d5b469..a4be44fc 100644 --- a/arduino-ide-extension/src/browser/boards/list-widget.tsx +++ b/arduino-ide-extension/src/browser/boards/list-widget.tsx @@ -4,7 +4,8 @@ import { Message } from '@phosphor/messaging'; import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { FilterableListContainer } from '../components/component-list/filterable-list-container'; -import { BoardsService } from '../../common/protocol/boards-service'; +import { BoardsService, Board, BoardPackage } from '../../common/protocol/boards-service'; +import { BoardsNotificationService } from '../boards-notification-service'; @injectable() export abstract class ListWidget extends ReactWidget { @@ -15,6 +16,9 @@ export abstract class ListWidget extends ReactWidget { @inject(WindowService) protected readonly windowService: WindowService; + @inject(BoardsNotificationService) + protected readonly boardsNotificationService: BoardsNotificationService; + constructor() { super(); const { id, title, iconClass } = this.widgetProps(); @@ -46,8 +50,19 @@ export abstract class ListWidget extends ReactWidget { } render(): React.ReactNode { + const boardsServiceDelegate = this.boardsService; + const boardsService: BoardsService = { + getAttachedBoards: () => boardsServiceDelegate.getAttachedBoards(), + selectBoard: (board: Board) => boardsServiceDelegate.selectBoard(board), + getSelectBoard: () => boardsServiceDelegate.getSelectBoard(), + search: (options: { query?: string }) => boardsServiceDelegate.search(options), + install: async (item: BoardPackage) => { + await boardsServiceDelegate.install(item); + this.boardsNotificationService.notifyBoardsInstalled(); + } + } return ; } diff --git a/arduino-ide-extension/src/browser/components/connected-boards.tsx b/arduino-ide-extension/src/browser/components/connected-boards.tsx index 7a95765f..163e2e72 100644 --- a/arduino-ide-extension/src/browser/components/connected-boards.tsx +++ b/arduino-ide-extension/src/browser/components/connected-boards.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { BoardsService, Board } from '../../common/protocol/boards-service'; // import { SelectBoardDialog } from './select-board-dialog'; import { QuickPickService } from '@theia/core/lib/common/quick-pick-service'; +import { BoardsNotificationService } from '../boards-notification-service'; export class ConnectedBoards extends React.Component { static TOOLBAR_ID: 'connected-boards-toolbar'; @@ -9,6 +10,8 @@ export class ConnectedBoards extends React.Component this.onBoardsInstalled()); } render(): React.ReactNode { @@ -44,14 +47,34 @@ export class ConnectedBoards extends React.Component !b.fqbn || b.name === "unknown"); + } + protected async reloadBoards() { - this.setState({ boardsLoading: true, boards: undefined, selection: undefined }); + const prevSelection = this.state.selection; + this.setState({ boardsLoading: true, boards: undefined, selection: "loading" }); const { boards } = await this.props.boardsService.getAttachedBoards() - this.setState({ boards, boardsLoading: false }); + this.setState({ boards, boardsLoading: false, selection: prevSelection }); if (boards) { this.setState({ selection: "0" }); await this.props.boardsService.selectBoard(boards[0]); + + const unknownBoards = this.findUnknownBoards(); + if (unknownBoards && unknownBoards.length > 0) { + this.props.onUnknownBoard(unknownBoards[0]); + } } } @@ -85,6 +108,11 @@ export class ConnectedBoards extends React.Component(); items.filter(pkg => !!pkg.installedVersion).forEach(pkg => pkg.boards.forEach(brd => idx.set(`${brd.name}`, brd) )); + if (idx.size === 0) { + this.props.onNoBoardsInstalled(); + return; + } + const selection = await this.props.quickPickService.show(Array.from(idx.keys())); if (!selection) { return; @@ -99,7 +127,10 @@ export namespace ConnectedBoards { export interface Props { readonly boardsService: BoardsService; + readonly boardsNotificationService: BoardsNotificationService; readonly quickPickService: QuickPickService; + readonly onNoBoardsInstalled: () => void; + readonly onUnknownBoard: (board: Board) => void; } export interface State { diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 29a646d4..cb46bb40 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -3,6 +3,7 @@ import { BoardsService, AttachedSerialBoard, AttachedNetworkBoard, BoardPackage, import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp } from './cli-protocol/core_pb'; import { CoreClientProvider } from './core-client-provider'; import { BoardListReq, BoardListResp } from './cli-protocol/board_pb'; +import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; @injectable() export class BoardsServiceImpl implements BoardsService { @@ -10,6 +11,9 @@ export class BoardsServiceImpl implements BoardsService { @inject(CoreClientProvider) protected readonly coreClientProvider: CoreClientProvider; + @inject(ToolOutputServiceServer) + protected readonly toolOutputService: ToolOutputServiceServer; + protected selectedBoard: Board | undefined; public async getAttachedBoards(): Promise<{ boards: Board[] }> { @@ -100,8 +104,8 @@ export class BoardsServiceImpl implements BoardsService { const resp = client.platformInstall(req); resp.on('data', (r: PlatformInstallResp) => { const prog = r.getProgress(); - if (prog) { - console.info(`downloading ${prog.getFile()}: ${prog.getCompleted()}%`) + if (prog && prog.getFile()) { + this.toolOutputService.publishNewOutput("board download", `downloading ${prog.getFile()}\n`) } }); await new Promise((resolve, reject) => { diff --git a/arduino-ide-extension/src/node/core-client-provider-impl.ts b/arduino-ide-extension/src/node/core-client-provider-impl.ts index 9b224d7d..4d75cadd 100644 --- a/arduino-ide-extension/src/node/core-client-provider-impl.ts +++ b/arduino-ide-extension/src/node/core-client-provider-impl.ts @@ -7,6 +7,8 @@ import { FileSystem } from '@theia/filesystem/lib/common'; import URI from '@theia/core/lib/common/uri'; import { CoreClientProvider, Client } from './core-client-provider'; import * as PQueue from 'p-queue'; +import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; +import { Instance } from './cli-protocol/common_pb'; @injectable() export class CoreClientProviderImpl implements CoreClientProvider { @@ -19,6 +21,9 @@ export class CoreClientProviderImpl implements CoreClientProvider { @inject(WorkspaceServiceExt) protected readonly workspaceServiceExt: WorkspaceServiceExt; + @inject(ToolOutputServiceServer) + protected readonly toolOutputService: ToolOutputServiceServer; + protected clients = new Map(); async getClient(workspaceRootOrResourceUri?: string): Promise { @@ -69,24 +74,20 @@ export class CoreClientProviderImpl implements CoreClientProvider { throw new Error(`Could not retrieve instance from the initialize response.`); } - // workaround to speed up startup on existing workspaces - const updateReq = new UpdateIndexReq(); - updateReq.setInstance(instance); - const updateResp = client.updateIndex(updateReq); - updateResp.on('data', (o: UpdateIndexResp) => { - const progress = o.getDownloadProgress(); - if (progress) { - if (progress.getCompleted()) { - console.log(`Download${progress.getFile() ? ` of ${progress.getFile()}` : ''} completed.`); - } else { - console.log(`Downloading${progress.getFile() ? ` ${progress.getFile()}:` : ''} ${progress.getDownloaded()}.`); - } + // in a seperate promise, try and update the index + let succeeded = true; + for (let i = 0; i < 10; i++) { + try { + await this.updateIndex(client, instance); + succeeded = true; + break; + } catch (e) { + this.toolOutputService.publishNewOutput("daemon", `Error while updating index in attempt ${i}: ${e}`); } - }); - await new Promise((resolve, reject) => { - updateResp.on('error', reject); - updateResp.on('end', resolve); - }); + } + if (!succeeded) { + this.toolOutputService.publishNewOutput("daemon", `Was unable to update the index. Please restart to try again.`); + } const result = { client, @@ -96,4 +97,23 @@ export class CoreClientProviderImpl implements CoreClientProvider { console.info(` <<< New client has been successfully created and cached for ${rootUri}.`); return result; } + + protected async updateIndex(client: ArduinoCoreClient, instance: Instance): Promise { + const updateReq = new UpdateIndexReq(); + updateReq.setInstance(instance); + const updateResp = client.updateIndex(updateReq); + updateResp.on('data', (o: UpdateIndexResp) => { + const progress = o.getDownloadProgress(); + if (progress) { + if (progress.getCompleted()) { + this.toolOutputService.publishNewOutput("daemon", `Download${progress.getFile() ? ` of ${progress.getFile()}` : ''} completed.\n`); + } + } + }); + await new Promise((resolve, reject) => { + updateResp.on('error', reject); + updateResp.on('end', resolve); + }); + } + } \ No newline at end of file diff --git a/known-issues.txt b/known-issues.txt new file mode 100644 index 00000000..250112b2 --- /dev/null +++ b/known-issues.txt @@ -0,0 +1,3 @@ +Known issues: + - arduino-cli does not get stopped reliably upon app shutdown + - startup time is not as fast we'd like to have it