diff --git a/arduino-debugger-extension/src/browser/arduino-variable-resolver.ts b/arduino-debugger-extension/src/browser/arduino-variable-resolver.ts index eb3028ef..faee5c9e 100644 --- a/arduino-debugger-extension/src/browser/arduino-variable-resolver.ts +++ b/arduino-debugger-extension/src/browser/arduino-variable-resolver.ts @@ -2,13 +2,13 @@ import { VariableContribution, VariableRegistry, Variable } from '@theia/variable-resolver/lib/browser'; import { injectable, inject } from 'inversify'; import { MessageService } from '@theia/core/lib/common/message-service'; -import { BoardsServiceClientImpl } from 'arduino-ide-extension/lib/browser/boards/boards-service-client-impl'; +import { BoardsServiceProvider } from 'arduino-ide-extension/lib/browser/boards/boards-service-provider'; @injectable() export class ArduinoVariableResolver implements VariableContribution { - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClient: BoardsServiceClientImpl; + @inject(BoardsServiceProvider) + protected readonly boardsServiceProvider: BoardsServiceProvider; @inject(MessageService) protected readonly messageService: MessageService @@ -27,7 +27,7 @@ export class ArduinoVariableResolver implements VariableContribution { } protected async resolveFqbn(): Promise { - const { boardsConfig } = this.boardsServiceClient; + const { boardsConfig } = this.boardsServiceProvider; if (!boardsConfig || !boardsConfig.selectedBoard) { this.messageService.error('No board selected. Please select a board for debugging.'); return undefined; @@ -36,7 +36,7 @@ export class ArduinoVariableResolver implements VariableContribution { } protected async resolvePort(): Promise { - const { boardsConfig } = this.boardsServiceClient; + const { boardsConfig } = this.boardsServiceProvider; if (!boardsConfig || !boardsConfig.selectedPort) { return undefined; } diff --git a/arduino-ide-extension/src/browser/arduino-daemon-client-impl.ts b/arduino-ide-extension/src/browser/arduino-daemon-client-impl.ts deleted file mode 100644 index f638bee9..00000000 --- a/arduino-ide-extension/src/browser/arduino-daemon-client-impl.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { injectable, inject } from 'inversify'; -import { ILogger } from '@theia/core/lib/common/logger'; -import { Event, Emitter } from '@theia/core/lib/common/event'; -import { MessageService } from '@theia/core/lib/common/message-service'; -import { ArduinoDaemonClient } from '../common/protocol'; - -@injectable() -export class ArduinoDaemonClientImpl implements ArduinoDaemonClient { - - @inject(ILogger) - protected readonly logger: ILogger; - - @inject(MessageService) - protected readonly messageService: MessageService; - - protected readonly onStartedEmitter = new Emitter(); - protected readonly onStoppedEmitter = new Emitter(); - protected _isRunning = false; - - notifyStopped(): void { - if (this._isRunning) { - this._isRunning = false; - this.onStoppedEmitter.fire(); - this.info('The CLI daemon process has stopped.'); - } - } - - notifyStarted(): void { - if (!this._isRunning) { - this._isRunning = true; - this.onStartedEmitter.fire(); - this.info('The CLI daemon process has started.'); - } - } - - get onDaemonStarted(): Event { - return this.onStartedEmitter.event; - } - - get onDaemonStopped(): Event { - return this.onStoppedEmitter.event; - } - - get isRunning(): boolean { - return this._isRunning; - } - - protected info(message: string): void { - this.messageService.info(message, { timeout: 3000 }); - this.logger.info(message); - } - -} diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index f9369b34..f57c26a0 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -24,7 +24,7 @@ import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-con import { inject, injectable, postConstruct } from 'inversify'; import * as React from 'react'; import { MainMenuManager } from '../common/main-menu-manager'; -import { BoardsService, BoardsServiceClient, CoreService, Port, SketchesService, ToolOutputServiceClient, ExecutableService } from '../common/protocol'; +import { BoardsService, CoreService, Port, SketchesService, ExecutableService } from '../common/protocol'; import { ArduinoDaemon } from '../common/protocol/arduino-daemon'; import { ConfigService } from '../common/protocol/config-service'; import { FileSystemExt } from '../common/protocol/filesystem-ext'; @@ -32,7 +32,7 @@ import { ArduinoCommands } from './arduino-commands'; import { BoardsConfig } from './boards/boards-config'; import { BoardsConfigDialog } from './boards/boards-config-dialog'; import { BoardsDataStore } from './boards/boards-data-store'; -import { BoardsServiceClientImpl } from './boards/boards-service-client-impl'; +import { BoardsServiceProvider } from './boards/boards-service-provider'; import { BoardsToolBarItem } from './boards/boards-toolbar-item'; import { EditorMode } from './editor-mode'; import { ArduinoMenus } from './menu/arduino-menus'; @@ -44,6 +44,9 @@ import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted import { FileService } from '@theia/filesystem/lib/browser/file-service'; const debounce = require('lodash.debounce'); +import { OutputService } from '../common/protocol/output-service'; +import { NotificationCenter } from './notification-center'; +import { Settings } from './contributions/settings'; @injectable() export class ArduinoFrontendContribution implements FrontendApplicationContribution, @@ -61,15 +64,8 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut @inject(CoreService) protected readonly coreService: CoreService; - @inject(ToolOutputServiceClient) - protected readonly toolOutputServiceClient: ToolOutputServiceClient; - - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClientImpl: BoardsServiceClientImpl; - - // Unused but do not remove it. It's required by DI, otherwise `init` method is not called. - @inject(BoardsServiceClient) - protected readonly boardsServiceClient: BoardsServiceClient; + @inject(BoardsServiceProvider) + protected readonly boardsServiceClientImpl: BoardsServiceProvider; @inject(SelectionService) protected readonly selectionService: SelectionService; @@ -151,6 +147,13 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut @inject(ExecutableService) protected executableService: ExecutableService; + @inject(OutputService) + protected readonly outputService: OutputService; + + @inject(NotificationCenter) + protected readonly notificationCenter: NotificationCenter; + + protected invalidConfigPopup: Promise | undefined; @postConstruct() protected async init(): Promise { @@ -198,6 +201,21 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut } } }); + this.notificationCenter.onConfigChanged(({ config }) => { + if (config) { + this.invalidConfigPopup = undefined; + } else { + if (!this.invalidConfigPopup) { + this.invalidConfigPopup = this.messageService.error(`Your CLI configuration is invalid. Do you want to correct it now?`, 'No', 'Yes') + .then(answer => { + if (answer === 'Yes') { + this.commandRegistry.executeCommand(Settings.Commands.OPEN_CLI_CONFIG.id) + } + this.invalidConfigPopup = undefined; + }); + } + } + }); } protected startLanguageServer = debounce((fqbn: string, name: string | undefined) => this.doStartLanguageServer(fqbn, name)); diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index d68d47e3..241b3b45 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -8,17 +8,14 @@ import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/w import { FrontendApplicationContribution, FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application' import { LibraryListWidget } from './library/library-list-widget'; import { ArduinoFrontendContribution } from './arduino-frontend-contribution'; -import { LibraryServiceServer, LibraryServiceServerPath } from '../common/protocol/library-service'; -import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service'; +import { LibraryService, LibraryServicePath } from '../common/protocol/library-service'; +import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service'; import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service'; import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl'; -import { CoreService, CoreServicePath, CoreServiceClient } from '../common/protocol/core-service'; +import { CoreService, CoreServicePath } from '../common/protocol/core-service'; import { BoardsListWidget } from './boards/boards-list-widget'; import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution'; -import { ToolOutputServiceClient } from '../common/protocol/tool-output-service'; -import { ToolOutputService } from '../common/protocol/tool-output-service'; -import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl'; -import { BoardsServiceClientImpl } from './boards/boards-service-client-impl'; +import { BoardsServiceProvider } from './boards/boards-service-provider'; import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { WorkspaceService } from './theia/workspace/workspace-service'; import { OutlineViewContribution as TheiaOutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution'; @@ -50,7 +47,7 @@ import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspa import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution'; import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl'; import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service'; -import { ConfigService, ConfigServicePath, ConfigServiceClient } from '../common/protocol/config-service'; +import { ConfigService, ConfigServicePath } from '../common/protocol/config-service'; import { MonitorWidget } from './monitor/monitor-widget'; import { MonitorViewContribution } from './monitor/monitor-view-contribution'; import { MonitorConnection } from './monitor/monitor-connection'; @@ -67,8 +64,7 @@ import { EditorMode } from './editor-mode'; import { ListItemRenderer } from './widgets/component-list/list-item-renderer'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service'; -import { ArduinoDaemonClientImpl } from './arduino-daemon-client-impl'; -import { ArduinoDaemonClient, ArduinoDaemonPath, ArduinoDaemon } from '../common/protocol/arduino-daemon'; +import { ArduinoDaemonPath, ArduinoDaemon } from '../common/protocol/arduino-daemon'; import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser'; import { EditorManager } from './theia/editor/editor-manager'; import { FrontendConnectionStatusService, ApplicationConnectionStatusContribution } from './theia/core/connection-status-service'; @@ -76,8 +72,6 @@ import { FrontendConnectionStatusService as TheiaFrontendConnectionStatusService, ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution } from '@theia/core/lib/browser/connection-status-service'; -import { ConfigServiceClientImpl } from './config-service-client-impl'; -import { CoreServiceClientImpl } from './core-service-client-impl'; import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater'; import { BoardsDataStore } from './boards/boards-data-store'; import { ILogger } from '@theia/core'; @@ -116,7 +110,6 @@ import { OutputWidget } from './theia/output/output-widget'; import { BurnBootloader } from './contributions/burn-bootloader'; import { ExamplesServicePath, ExamplesService } from '../common/protocol/examples-service'; import { BuiltInExamples, LibraryExamples } from './contributions/examples'; -import { LibraryServiceProvider } from './library/library-service-provider'; import { IncludeLibrary } from './contributions/include-library'; import { OutputChannelManager as TheiaOutputChannelManager } from '@theia/output/lib/common/output-channel'; import { OutputChannelManager } from './theia/output/output-channel'; @@ -124,6 +117,10 @@ import { OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl, Ou import { ExecutableService, ExecutableServicePath } from '../common/protocol'; import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service'; +import { OutputServiceImpl } from './output-service-impl'; +import { OutputServicePath, OutputService } from '../common/protocol/output-service'; +import { NotificationCenter } from './notification-center'; +import { NotificationServicePath, NotificationServiceServer } from '../common/protocol'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -153,8 +150,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ListItemRenderer).toSelf().inSingletonScope(); // Library service - bind(LibraryServiceProvider).toSelf().inSingletonScope(); - bind(LibraryServiceServer).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServiceServerPath)).inSingletonScope(); + bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope(); // Library list widget bind(LibraryListWidget).toSelf(); @@ -170,34 +166,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(SketchesServiceClientImpl).toSelf().inSingletonScope(); // Config service - bind(ConfigService).toDynamicValue(context => { - const connection = context.container.get(WebSocketConnectionProvider); - const client = context.container.get(ConfigServiceClientImpl); - return connection.createProxy(ConfigServicePath, client); - }).inSingletonScope(); - bind(ConfigServiceClientImpl).toSelf().inSingletonScope(); - bind(ConfigServiceClient).toDynamicValue(context => { - const client = context.container.get(ConfigServiceClientImpl); - WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath, client); - return client; - }).inSingletonScope(); + bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope(); // Boards service - bind(BoardsService).toDynamicValue(context => { - const connection = context.container.get(WebSocketConnectionProvider); - const client = context.container.get(BoardsServiceClientImpl); - return connection.createProxy(BoardsServicePath, client); - }).inSingletonScope(); + bind(BoardsService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath)).inSingletonScope(); // Boards service client to receive and delegate notifications from the backend. - bind(BoardsServiceClientImpl).toSelf().inSingletonScope(); - bind(FrontendApplicationContribution).toService(BoardsServiceClientImpl); - bind(BoardsServiceClient).toDynamicValue(async context => { - const client = context.container.get(BoardsServiceClientImpl); - const service = context.container.get(BoardsService); - await client.init(service); - WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath, client); - return client; - }).inSingletonScope(); + bind(BoardsServiceProvider).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(BoardsServiceProvider); // To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board. bind(FrontendApplicationContribution).to(BoardsDataMenuUpdater).inSingletonScope(); @@ -230,26 +205,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { }) // Core service - bind(CoreService).toDynamicValue(context => { - const connection = context.container.get(WebSocketConnectionProvider); - const client = context.container.get(CoreServiceClientImpl); - return connection.createProxy(CoreServicePath, client); - }).inSingletonScope(); - // Core service client to receive and delegate notifications when the index or the library index has been updated. - bind(CoreServiceClientImpl).toSelf().inSingletonScope(); - bind(CoreServiceClient).toDynamicValue(context => { - const client = context.container.get(CoreServiceClientImpl); - WebSocketConnectionProvider.createProxy(context.container, CoreServicePath, client); - return client; - }).inSingletonScope(); - - // Tool output service client - bind(ToolOutputServiceClientImpl).toSelf().inSingletonScope(); - bind(ToolOutputServiceClient).toDynamicValue(context => { - const client = context.container.get(ToolOutputServiceClientImpl); - WebSocketConnectionProvider.createProxy(context.container, ToolOutputService.SERVICE_PATH, client); - return client; - }).inSingletonScope(); + bind(CoreService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, CoreServicePath)).inSingletonScope(); // Serial monitor bind(MonitorModel).toSelf().inSingletonScope(); @@ -341,18 +297,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ShellLayoutRestorer).toSelf().inSingletonScope(); rebind(TheiaShellLayoutRestorer).toService(ShellLayoutRestorer); - // Arduino daemon client. Receives notifications from the backend if the CLI daemon process has been restarted. - bind(ArduinoDaemon).toDynamicValue(context => { - const connection = context.container.get(WebSocketConnectionProvider); - const client = context.container.get(ArduinoDaemonClientImpl); - return connection.createProxy(ArduinoDaemonPath, client); - }).inSingletonScope(); - bind(ArduinoDaemonClientImpl).toSelf().inSingletonScope(); - bind(ArduinoDaemonClient).toDynamicValue(context => { - const client = context.container.get(ArduinoDaemonClientImpl); - WebSocketConnectionProvider.createProxy(context.container, ArduinoDaemonPath, client); - return client; - }).inSingletonScope(); + bind(ArduinoDaemon).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ArduinoDaemonPath)).inSingletonScope(); // File-system extension bind(FileSystemExt).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, FileSystemExtPath)).inSingletonScope(); @@ -379,4 +324,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { Contribution.configure(bind, BuiltInExamples); Contribution.configure(bind, LibraryExamples); Contribution.configure(bind, IncludeLibrary); + + bind(OutputServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, outputService) => { + WebSocketConnectionProvider.createProxy(container, OutputServicePath, outputService); + return outputService; + }); + bind(OutputService).toService(OutputServiceImpl); + + bind(NotificationCenter).toSelf().inSingletonScope(); + bind(NotificationServiceServer).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, NotificationServicePath)).inSingletonScope(); }); diff --git a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts index 50a06166..94393f4e 100644 --- a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts +++ b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts @@ -1,8 +1,8 @@ import { injectable, inject } from 'inversify'; import { MessageService } from '@theia/core/lib/common/message-service'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; -import { BoardsService, Board } from '../../common/protocol/boards-service'; -import { BoardsServiceClientImpl } from './boards-service-client-impl'; +import { BoardsService, BoardsPackage } from '../../common/protocol/boards-service'; +import { BoardsServiceProvider } from './boards-service-provider'; import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution'; import { InstallationProgressDialog } from '../widgets/progress-dialog'; import { BoardsConfig } from './boards-config'; @@ -20,8 +20,8 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution { @inject(BoardsService) protected readonly boardsService: BoardsService; - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClient: BoardsServiceClientImpl; + @inject(BoardsServiceProvider) + protected readonly boardsServiceClient: BoardsServiceProvider; @inject(BoardsListWidgetFrontendContribution) protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution; @@ -36,7 +36,7 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution { if (selectedBoard) { this.boardsService.search({}).then(packages => { const candidates = packages - .filter(pkg => pkg.boards.some(board => Board.sameAs(board, selectedBoard))) + .filter(pkg => BoardsPackage.contains(selectedBoard, pkg)) .filter(({ installable, installedVersion }) => installable && !installedVersion); for (const candidate of candidates) { // tslint:disable-next-line:max-line-length diff --git a/arduino-ide-extension/src/browser/boards/boards-config-dialog-widget.tsx b/arduino-ide-extension/src/browser/boards/boards-config-dialog-widget.tsx index b08157b3..94ae04da 100644 --- a/arduino-ide-extension/src/browser/boards/boards-config-dialog-widget.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-config-dialog-widget.tsx @@ -4,9 +4,8 @@ import { Emitter } from '@theia/core/lib/common/event'; import { ReactWidget, Message } from '@theia/core/lib/browser'; import { BoardsService } from '../../common/protocol/boards-service'; import { BoardsConfig } from './boards-config'; -import { BoardsServiceClientImpl } from './boards-service-client-impl'; -import { CoreServiceClientImpl } from '../core-service-client-impl'; -import { ArduinoDaemonClientImpl } from '../arduino-daemon-client-impl'; +import { BoardsServiceProvider } from './boards-service-provider'; +import { NotificationCenter } from '../notification-center'; @injectable() export class BoardsConfigDialogWidget extends ReactWidget { @@ -14,14 +13,11 @@ export class BoardsConfigDialogWidget extends ReactWidget { @inject(BoardsService) protected readonly boardsService: BoardsService; - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClient: BoardsServiceClientImpl; + @inject(BoardsServiceProvider) + protected readonly boardsServiceClient: BoardsServiceProvider; - @inject(CoreServiceClientImpl) - protected readonly coreServiceClient: CoreServiceClientImpl; - - @inject(ArduinoDaemonClientImpl) - protected readonly daemonClient: ArduinoDaemonClientImpl; + @inject(NotificationCenter) + protected readonly notificationCenter: NotificationCenter; protected readonly onBoardConfigChangedEmitter = new Emitter(); readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event; @@ -44,10 +40,8 @@ export class BoardsConfigDialogWidget extends ReactWidget { protected render(): React.ReactNode { return
; diff --git a/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts b/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts index 92495b2a..0b218c6c 100644 --- a/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts +++ b/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts @@ -4,7 +4,7 @@ import { AbstractDialog, DialogProps, Widget, DialogError } from '@theia/core/li import { BoardsService } from '../../common/protocol/boards-service'; import { BoardsConfig } from './boards-config'; import { BoardsConfigDialogWidget } from './boards-config-dialog-widget'; -import { BoardsServiceClientImpl } from './boards-service-client-impl'; +import { BoardsServiceProvider } from './boards-service-provider'; @injectable() export class BoardsConfigDialogProps extends DialogProps { @@ -19,8 +19,8 @@ export class BoardsConfigDialog extends AbstractDialog { @inject(BoardsService) protected readonly boardService: BoardsService; - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClient: BoardsServiceClientImpl; + @inject(BoardsServiceProvider) + protected readonly boardsServiceClient: BoardsServiceProvider; protected config: BoardsConfig.Config = {}; diff --git a/arduino-ide-extension/src/browser/boards/boards-config.tsx b/arduino-ide-extension/src/browser/boards/boards-config.tsx index 6ae778f2..e01d2604 100644 --- a/arduino-ide-extension/src/browser/boards/boards-config.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-config.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; -import { DisposableCollection } from '@theia/core'; -import { BoardsService, Board, Port, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service'; -import { BoardsServiceClientImpl } from './boards-service-client-impl'; -import { CoreServiceClientImpl } from '../core-service-client-impl'; -import { ArduinoDaemonClientImpl } from '../arduino-daemon-client-impl'; +import { notEmpty } from '@theia/core/lib/common/objects'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Board, Port, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service'; +import { BoardsServiceProvider } from './boards-service-provider'; +import { NotificationCenter } from '../notification-center'; +import { MaybePromise } from '@theia/core'; export namespace BoardsConfig { @@ -13,10 +14,8 @@ export namespace BoardsConfig { } export interface Props { - readonly boardsService: BoardsService; - readonly boardsServiceClient: BoardsServiceClientImpl; - readonly coreServiceClient: CoreServiceClientImpl; - readonly daemonClient: ArduinoDaemonClientImpl; + readonly boardsServiceProvider: BoardsServiceProvider; + readonly notificationCenter: NotificationCenter; readonly onConfigChange: (config: Config) => void; readonly onFocusNodeSet: (element: HTMLElement | undefined) => void; } @@ -70,7 +69,7 @@ export class BoardsConfig extends React.Component this.updatePorts(ports)); - const { boardsServiceClient, coreServiceClient, daemonClient } = this.props; + this.updatePorts(this.props.boardsServiceProvider.availableBoards.map(({ port }) => port).filter(notEmpty)); this.toDispose.pushAll([ - boardsServiceClient.onAttachedBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)), - boardsServiceClient.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => { + this.props.notificationCenter.onAttachedBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)), + this.props.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => { this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged()); }), - boardsServiceClient.onBoardsPackageInstalled(() => this.updateBoards(this.state.query)), - boardsServiceClient.onBoardsPackageUninstalled(() => this.updateBoards(this.state.query)), - coreServiceClient.onIndexUpdated(() => this.updateBoards(this.state.query)), - daemonClient.onDaemonStarted(() => this.updateBoards(this.state.query)), - daemonClient.onDaemonStopped(() => this.setState({ searchResults: [] })) + this.props.notificationCenter.onPlatformInstalled(() => this.updateBoards(this.state.query)), + this.props.notificationCenter.onPlatformUninstalled(() => this.updateBoards(this.state.query)), + this.props.notificationCenter.onIndexUpdated(() => this.updateBoards(this.state.query)), + this.props.notificationCenter.onDaemonStarted(() => this.updateBoards(this.state.query)), + this.props.notificationCenter.onDaemonStopped(() => this.setState({ searchResults: [] })) ]); } @@ -127,14 +125,14 @@ export class BoardsConfig extends React.Component> => { - return this.props.boardsServiceClient.searchBoards(options); + return this.props.boardsServiceProvider.searchBoards(options); } - protected get availablePorts(): Promise { - return this.props.boardsService.getAvailablePorts(); + protected get availablePorts(): MaybePromise { + return this.props.boardsServiceProvider.availableBoards.map(({ port }) => port).filter(notEmpty); } - protected queryPorts = async (availablePorts: Promise = this.availablePorts) => { + protected queryPorts = async (availablePorts: MaybePromise = this.availablePorts) => { const ports = await availablePorts; return { knownPorts: ports.sort(Port.compare) }; } diff --git a/arduino-ide-extension/src/browser/boards/boards-data-menu-updater.ts b/arduino-ide-extension/src/browser/boards/boards-data-menu-updater.ts index 30169a3b..a0c68738 100644 --- a/arduino-ide-extension/src/browser/boards/boards-data-menu-updater.ts +++ b/arduino-ide-extension/src/browser/boards/boards-data-menu-updater.ts @@ -3,7 +3,7 @@ import { inject, injectable } from 'inversify'; import { CommandRegistry } from '@theia/core/lib/common/command'; import { MenuModelRegistry, MenuNode } from '@theia/core/lib/common/menu'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; -import { BoardsServiceClientImpl } from './boards-service-client-impl'; +import { BoardsServiceProvider } from './boards-service-provider'; import { Board, ConfigOption, Programmer } from '../../common/protocol'; import { FrontendApplicationContribution } from '@theia/core/lib/browser'; import { BoardsDataStore } from './boards-data-store'; @@ -25,8 +25,8 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution { @inject(BoardsDataStore) protected readonly boardsDataStore: BoardsDataStore; - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClient: BoardsServiceClientImpl; + @inject(BoardsServiceProvider) + protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); protected readonly toDisposeOnBoardChange = new DisposableCollection(); diff --git a/arduino-ide-extension/src/browser/boards/boards-data-store.ts b/arduino-ide-extension/src/browser/boards/boards-data-store.ts index 47163355..78ff28b4 100644 --- a/arduino-ide-extension/src/browser/boards/boards-data-store.ts +++ b/arduino-ide-extension/src/browser/boards/boards-data-store.ts @@ -5,8 +5,8 @@ import { MaybePromise } from '@theia/core/lib/common/types'; import { Event, Emitter } from '@theia/core/lib/common/event'; import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser'; import { notEmpty } from '../../common/utils'; -import { BoardsServiceClientImpl } from './boards-service-client-impl'; import { BoardsService, ConfigOption, Installable, BoardDetails, Programmer } from '../../common/protocol'; +import { NotificationCenter } from '../notification-center'; @injectable() export class BoardsDataStore implements FrontendApplicationContribution { @@ -18,8 +18,8 @@ export class BoardsDataStore implements FrontendApplicationContribution { @inject(BoardsService) protected readonly boardsService: BoardsService; - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClient: BoardsServiceClientImpl; + @inject(NotificationCenter) + protected readonly notificationCenter: NotificationCenter; @inject(LocalStorageService) protected readonly storageService: LocalStorageService; @@ -27,7 +27,7 @@ export class BoardsDataStore implements FrontendApplicationContribution { protected readonly onChangedEmitter = new Emitter(); onStart(): void { - this.boardsServiceClient.onBoardsPackageInstalled(async ({ item }) => { + this.notificationCenter.onPlatformInstalled(async ({ item }) => { const { installedVersion: version } = item; if (!version) { return; @@ -76,7 +76,8 @@ export class BoardsDataStore implements FrontendApplicationContribution { const key = this.getStorageKey(fqbn, version); let data = await this.storageService.getData(key, undefined); if (data) { - if (data.programmers !== undefined) { // to be backward compatible. We did not save the `programmers` into the `localStorage`. + // If `configOptions` is empty we rather reload the data. See arduino/arduino-cli#954 and arduino/arduino-cli#955. + if (data.configOptions.length && data.programmers !== undefined) { // to be backward compatible. We did not save the `programmers` into the `localStorage`. return data; } } diff --git a/arduino-ide-extension/src/browser/boards/boards-list-widget.ts b/arduino-ide-extension/src/browser/boards/boards-list-widget.ts index c48e76c5..4509a5ca 100644 --- a/arduino-ide-extension/src/browser/boards/boards-list-widget.ts +++ b/arduino-ide-extension/src/browser/boards/boards-list-widget.ts @@ -1,4 +1,4 @@ -import { inject, injectable } from 'inversify'; +import { inject, injectable, postConstruct } from 'inversify'; import { BoardsPackage, BoardsService } from '../../common/protocol/boards-service'; import { ListWidget } from '../widgets/component-list/list-widget'; import { ListItemRenderer } from '../widgets/component-list/list-item-renderer'; @@ -24,4 +24,13 @@ export class BoardsListWidget extends ListWidget { }); } + @postConstruct() + protected init(): void { + super.init(); + this.toDispose.pushAll([ + this.notificationCenter.onPlatformInstalled(() => this.refresh(undefined)), + this.notificationCenter.onPlatformUninstalled(() => this.refresh(undefined)), + ]); + } + } diff --git a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts similarity index 89% rename from arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts rename to arduino-ide-extension/src/browser/boards/boards-service-provider.ts index 679c1270..b69da254 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts @@ -10,14 +10,12 @@ import { Board, BoardsService, BoardsPackage, - InstalledEvent, - UninstalledEvent, - BoardsServiceClient, AttachedBoardsChangeEvent } from '../../common/protocol'; import { BoardsConfig } from './boards-config'; import { naturalCompare } from '../../common/utils'; import { compareAnything } from '../theia/monaco/comparers'; +import { NotificationCenter } from '../notification-center'; interface BoardMatch { readonly board: Board & Readonly<{ packageName: string }>; @@ -25,7 +23,7 @@ interface BoardMatch { } @injectable() -export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApplicationContribution { +export class BoardsServiceProvider implements FrontendApplicationContribution { @inject(ILogger) protected logger: ILogger; @@ -36,9 +34,12 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp @inject(StorageService) protected storageService: StorageService; - protected readonly onBoardsPackageInstalledEmitter = new Emitter>(); - protected readonly onBoardsPackageUninstalledEmitter = new Emitter>(); - protected readonly onAttachedBoardsChangedEmitter = new Emitter(); + @inject(BoardsService) + protected boardsService: BoardsService; + + @inject(NotificationCenter) + protected notificationCenter: NotificationCenter; + protected readonly onBoardsConfigChangedEmitter = new Emitter(); protected readonly onAvailableBoardsChangedEmitter = new Emitter(); @@ -54,14 +55,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp protected _attachedBoards: Board[] = []; // This does not contain the `Unknown` boards. They're visible from the available ports only. protected _availablePorts: Port[] = []; protected _availableBoards: AvailableBoard[] = []; - protected boardsService: BoardsService; - /** - * Event when the state of the attached/detached boards has changed. For instance, the user have detached a physical board. - */ - readonly onAttachedBoardsChanged = this.onAttachedBoardsChangedEmitter.event; - readonly onBoardsPackageInstalled = this.onBoardsPackageInstalledEmitter.event; - readonly onBoardsPackageUninstalled = this.onBoardsPackageUninstalledEmitter.event; /** * Unlike `onAttachedBoardsChanged` this even fires when the user modifies the selected board in the IDE.\ * This even also fires, when the boards package was not available for the currently selected board, @@ -72,33 +66,72 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event; readonly onAvailableBoardsChanged = this.onAvailableBoardsChangedEmitter.event; - async onStart(): Promise { - return this.loadState(); - } + onStart(): void { + this.notificationCenter.onAttachedBoardsChanged(this.notifyAttachedBoardsChanged.bind(this)); + this.notificationCenter.onPlatformInstalled(this.notifyPlatformInstalled.bind(this)); + this.notificationCenter.onPlatformUninstalled(this.notifyPlatformUninstalled.bind(this)); - /** - * When the FE connects to the BE, the BE stets the known boards and ports.\ - * This is a DI workaround for not being able to inject the service into the client. - */ - async init(boardsService: BoardsService): Promise { - this.boardsService = boardsService; - const [attachedBoards, availablePorts] = await Promise.all([ + Promise.all([ this.boardsService.getAttachedBoards(), - this.boardsService.getAvailablePorts() - ]); - this._attachedBoards = attachedBoards; - this._availablePorts = availablePorts; - this.reconcileAvailableBoards().then(() => this.tryReconnect()); + this.boardsService.getAvailablePorts(), + this.loadState() + ]).then(([attachedBoards, availablePorts]) => { + this._attachedBoards = attachedBoards; + this._availablePorts = availablePorts; + this.reconcileAvailableBoards().then(() => this.tryReconnect()); + }); } - notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { - this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event)); + protected notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { + if (!AttachedBoardsChangeEvent.isEmpty(event)) { + this.logger.info('Attached boards and available ports changed:'); + this.logger.info(AttachedBoardsChangeEvent.toString(event)); + this.logger.info(`------------------------------------------`); + } this._attachedBoards = event.newState.boards; - this.onAttachedBoardsChangedEmitter.fire(event); this._availablePorts = event.newState.ports; this.reconcileAvailableBoards().then(() => this.tryReconnect()); } + protected notifyPlatformInstalled(event: { item: BoardsPackage }): void { + this.logger.info('Boards package installed: ', JSON.stringify(event)); + const { selectedBoard } = this.boardsConfig; + const { installedVersion, id } = event.item; + if (selectedBoard) { + const installedBoard = event.item.boards.find(({ name }) => name === selectedBoard.name); + if (installedBoard && (!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn)) { + this.logger.info(`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`); + this.boardsConfig = { + ...this.boardsConfig, + selectedBoard: installedBoard + }; + return; + } + // Trigger a board re-set. See: https://github.com/arduino/arduino-cli/issues/954 + // E.g: install `adafruit:avr`, then select `adafruit:avr:adafruit32u4` board, and finally install the required `arduino:avr` + this.boardsConfig = this.boardsConfig; + } + } + + protected notifyPlatformUninstalled(event: { item: BoardsPackage }): void { + this.logger.info('Boards package uninstalled: ', JSON.stringify(event)); + const { selectedBoard } = this.boardsConfig; + if (selectedBoard && selectedBoard.fqbn) { + const uninstalledBoard = event.item.boards.find(({ name }) => name === selectedBoard.name); + if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) { + this.logger.info(`Board package ${event.item.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.`); + const selectedBoardWithoutFqbn = { + name: selectedBoard.name + // No FQBN + }; + this.boardsConfig = { + ...this.boardsConfig, + selectedBoard: selectedBoardWithoutFqbn + }; + } + } + } + protected async tryReconnect(): Promise { if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) { for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) { @@ -127,43 +160,6 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp return false; } - notifyInstalled(event: InstalledEvent): void { - this.logger.info('Boards package installed: ', JSON.stringify(event)); - this.onBoardsPackageInstalledEmitter.fire(event); - const { selectedBoard } = this.boardsConfig; - const { installedVersion, id } = event.item; - if (selectedBoard) { - const installedBoard = event.item.boards.find(({ name }) => name === selectedBoard.name); - if (installedBoard && (!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn)) { - this.logger.info(`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`); - this.boardsConfig = { - ...this.boardsConfig, - selectedBoard: installedBoard - }; - } - } - } - - notifyUninstalled(event: UninstalledEvent): void { - this.logger.info('Boards package uninstalled: ', JSON.stringify(event)); - this.onBoardsPackageUninstalledEmitter.fire(event); - const { selectedBoard } = this.boardsConfig; - if (selectedBoard && selectedBoard.fqbn) { - const uninstalledBoard = event.item.boards.find(({ name }) => name === selectedBoard.name); - if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) { - this.logger.info(`Board package ${event.item.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.`); - const selectedBoardWithoutFqbn = { - name: selectedBoard.name - // No FQBN - }; - this.boardsConfig = { - ...this.boardsConfig, - selectedBoard: selectedBoardWithoutFqbn - }; - } - } - } - set boardsConfig(config: BoardsConfig.Config) { this.doSetBoardsConfig(config); this.saveState().finally(() => this.reconcileAvailableBoards().finally(() => this.onBoardsConfigChangedEmitter.fire(this._boardsConfig))); diff --git a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx index 5e34e1fb..8666926a 100644 --- a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx @@ -5,7 +5,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { Port } from '../../common/protocol'; import { BoardsConfig } from './boards-config'; import { ArduinoCommands } from '../arduino-commands'; -import { BoardsServiceClientImpl, AvailableBoard } from './boards-service-client-impl'; +import { BoardsServiceProvider, AvailableBoard } from './boards-service-provider'; export interface BoardsDropDownListCoords { readonly top: number; @@ -181,7 +181,7 @@ export class BoardsToolBarItem extends React.Component this.update(this.availableBoards)); + this.notificationCenter.onIndexUpdated(() => this.update(this.availableBoards)); this.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.update(availableBoards)); this.update(this.boardsServiceClient.availableBoards); } diff --git a/arduino-ide-extension/src/browser/config-service-client-impl.ts b/arduino-ide-extension/src/browser/config-service-client-impl.ts deleted file mode 100644 index 6351bb59..00000000 --- a/arduino-ide-extension/src/browser/config-service-client-impl.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { injectable, inject } from 'inversify'; -import { ILogger } from '@theia/core/lib/common/logger'; -import { Event, Emitter } from '@theia/core/lib/common/event'; -import { CommandService } from '@theia/core/lib/common/command'; -import { MessageService } from '@theia/core/lib/common/message-service'; -import { ConfigServiceClient, Config } from '../common/protocol'; -import { Settings } from './contributions/settings'; - -@injectable() -export class ConfigServiceClientImpl implements ConfigServiceClient { - - @inject(CommandService) - protected readonly commandService: CommandService; - - @inject(ILogger) - protected readonly logger: ILogger; - - @inject(MessageService) - protected readonly messageService: MessageService; - - protected readonly onConfigChangedEmitter = new Emitter(); - protected invalidConfigPopup: Promise | undefined; - - notifyConfigChanged(config: Config): void { - this.invalidConfigPopup = undefined; - this.info(`The CLI configuration has been successfully reloaded.`); - this.onConfigChangedEmitter.fire(config); - } - - notifyInvalidConfig(): void { - if (!this.invalidConfigPopup) { - this.invalidConfigPopup = this.messageService.error(`Your CLI configuration is invalid. Do you want to correct it now?`, 'No', 'Yes') - .then(answer => { - if (answer === 'Yes') { - this.commandService.executeCommand(Settings.Commands.OPEN_CLI_CONFIG.id) - } - this.invalidConfigPopup = undefined; - }) - - } - } - - get onConfigChanged(): Event { - return this.onConfigChangedEmitter.event; - } - - protected info(message: string): void { - this.messageService.info(message, { timeout: 3000 }); - this.logger.info(message); - } - -} diff --git a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts index e2ff53d7..d4968fb1 100644 --- a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts +++ b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts @@ -4,7 +4,7 @@ import { CoreService } from '../../common/protocol'; import { ArduinoMenus } from '../menu/arduino-menus'; import { BoardsDataStore } from '../boards/boards-data-store'; import { MonitorConnection } from '../monitor/monitor-connection'; -import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution'; @injectable() @@ -19,8 +19,8 @@ export class BurnBootloader extends SketchContribution { @inject(BoardsDataStore) protected readonly boardsDataStore: BoardsDataStore; - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClientImpl: BoardsServiceClientImpl; + @inject(BoardsServiceProvider) + protected readonly boardsServiceClientImpl: BoardsServiceProvider; @inject(OutputChannelManager) protected readonly outputChannelManager: OutputChannelManager; diff --git a/arduino-ide-extension/src/browser/contributions/examples.ts b/arduino-ide-extension/src/browser/contributions/examples.ts index 1756d0d1..e8a2538d 100644 --- a/arduino-ide-extension/src/browser/contributions/examples.ts +++ b/arduino-ide-extension/src/browser/contributions/examples.ts @@ -5,10 +5,10 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa import { OpenSketch } from './open-sketch'; import { ArduinoMenus } from '../menu/arduino-menus'; import { MainMenuManager } from '../../common/main-menu-manager'; -import { LibraryServiceProvider } from '../library/library-service-provider'; -import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { ExamplesService, ExampleContainer } from '../../common/protocol/examples-service'; import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution'; +import { NotificationCenter } from '../notification-center'; @injectable() export abstract class Examples extends SketchContribution { @@ -25,8 +25,8 @@ export abstract class Examples extends SketchContribution { @inject(ExamplesService) protected readonly examplesService: ExamplesService; - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClient: BoardsServiceClientImpl; + @inject(BoardsServiceProvider) + protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly toDispose = new DisposableCollection(); @@ -113,15 +113,15 @@ export class BuiltInExamples extends Examples { @injectable() export class LibraryExamples extends Examples { - @inject(LibraryServiceProvider) - protected readonly libraryServiceProvider: LibraryServiceProvider; + @inject(NotificationCenter) + protected readonly notificationCenter: NotificationCenter; protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); onStart(): void { this.register(); // no `await` - this.libraryServiceProvider.onLibraryPackageInstalled(() => this.register()); - this.libraryServiceProvider.onLibraryPackageUninstalled(() => this.register()); + this.notificationCenter.onLibraryInstalled(() => this.register()); + this.notificationCenter.onLibraryUninstalled(() => this.register()); } protected handleBoardChanged(fqbn: string | undefined): void { diff --git a/arduino-ide-extension/src/browser/contributions/include-library.ts b/arduino-ide-extension/src/browser/contributions/include-library.ts index 78505fe3..6ad8756a 100644 --- a/arduino-ide-extension/src/browser/contributions/include-library.ts +++ b/arduino-ide-extension/src/browser/contributions/include-library.ts @@ -6,12 +6,12 @@ import { EditorManager } from '@theia/editor/lib/browser'; import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { ArduinoMenus } from '../menu/arduino-menus'; -import { LibraryPackage, LibraryLocation } from '../../common/protocol'; +import { LibraryPackage, LibraryLocation, LibraryService } from '../../common/protocol'; import { MainMenuManager } from '../../common/main-menu-manager'; import { LibraryListWidget } from '../library/library-list-widget'; -import { LibraryServiceProvider } from '../library/library-service-provider'; -import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { SketchContribution, Command, CommandRegistry } from './contribution'; +import { NotificationCenter } from '../notification-center'; @injectable() export class IncludeLibrary extends SketchContribution { @@ -28,11 +28,14 @@ export class IncludeLibrary extends SketchContribution { @inject(EditorManager) protected readonly editorManager: EditorManager; - @inject(LibraryServiceProvider) - protected readonly libraryServiceProvider: LibraryServiceProvider; + @inject(NotificationCenter) + protected readonly notificationCenter: NotificationCenter; - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClient: BoardsServiceClientImpl; + @inject(BoardsServiceProvider) + protected readonly boardsServiceClient: BoardsServiceProvider; + + @inject(LibraryService) + protected readonly libraryService: LibraryService; protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); protected readonly toDispose = new DisposableCollection(); @@ -40,8 +43,8 @@ export class IncludeLibrary extends SketchContribution { onStart(): void { this.updateMenuActions(); this.boardsServiceClient.onBoardsConfigChanged(() => this.updateMenuActions()) - this.libraryServiceProvider.onLibraryPackageInstalled(() => this.updateMenuActions()); - this.libraryServiceProvider.onLibraryPackageUninstalled(() => this.updateMenuActions()); + this.notificationCenter.onLibraryInstalled(() => this.updateMenuActions()); + this.notificationCenter.onLibraryUninstalled(() => this.updateMenuActions()); } registerCommands(registry: CommandRegistry): void { @@ -62,7 +65,7 @@ export class IncludeLibrary extends SketchContribution { const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn; // Do not show board specific examples, when no board is selected. if (fqbn) { - libraries.push(...await this.libraryServiceProvider.list({ fqbn })); + libraries.push(...await this.libraryService.list({ fqbn })); } // `Include Library` submenu diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index 027eca7e..bae20667 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -5,7 +5,7 @@ import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { BoardsDataStore } from '../boards/boards-data-store'; import { MonitorConnection } from '../monitor/monitor-connection'; -import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution'; @injectable() @@ -20,8 +20,8 @@ export class UploadSketch extends SketchContribution { @inject(BoardsDataStore) protected readonly boardsDataStore: BoardsDataStore; - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClientImpl: BoardsServiceClientImpl; + @inject(BoardsServiceProvider) + protected readonly boardsServiceClientImpl: BoardsServiceProvider; @inject(OutputChannelManager) protected readonly outputChannelManager: OutputChannelManager; diff --git a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts index 9ac9e369..a4e99b76 100644 --- a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts @@ -4,7 +4,7 @@ import { CoreService } from '../../common/protocol'; import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { BoardsDataStore } from '../boards/boards-data-store'; -import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution'; @injectable() @@ -16,8 +16,8 @@ export class VerifySketch extends SketchContribution { @inject(BoardsDataStore) protected readonly boardsDataStore: BoardsDataStore; - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClientImpl: BoardsServiceClientImpl; + @inject(BoardsServiceProvider) + protected readonly boardsServiceClientImpl: BoardsServiceProvider; @inject(OutputChannelManager) protected readonly outputChannelManager: OutputChannelManager; diff --git a/arduino-ide-extension/src/browser/core-service-client-impl.ts b/arduino-ide-extension/src/browser/core-service-client-impl.ts deleted file mode 100644 index 9f2f3690..00000000 --- a/arduino-ide-extension/src/browser/core-service-client-impl.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { injectable, inject } from 'inversify'; -import { Emitter, Event } from '@theia/core/lib/common/event'; -import { ILogger } from '@theia/core/lib/common/logger'; -import { MessageService } from '@theia/core/lib/common/message-service'; -import { LocalStorageService } from '@theia/core/lib/browser/storage-service'; -import { CoreServiceClient } from '../common/protocol'; - -@injectable() -export class CoreServiceClientImpl implements CoreServiceClient { - - @inject(ILogger) - protected logger: ILogger; - - @inject(MessageService) - protected messageService: MessageService; - - @inject(LocalStorageService) - protected storageService: LocalStorageService; - - protected readonly onIndexUpdatedEmitter = new Emitter(); - - notifyIndexUpdated(): void { - this.info('Index has been updated.'); - this.onIndexUpdatedEmitter.fire(); - } - - get onIndexUpdated(): Event { - return this.onIndexUpdatedEmitter.event; - } - - protected info(message: string): void { - this.messageService.info(message, { timeout: 3000 }); - this.logger.info(message); - } - -} diff --git a/arduino-ide-extension/src/browser/library/library-list-widget.ts b/arduino-ide-extension/src/browser/library/library-list-widget.ts index 0a6b5851..60871e4e 100644 --- a/arduino-ide-extension/src/browser/library/library-list-widget.ts +++ b/arduino-ide-extension/src/browser/library/library-list-widget.ts @@ -1,8 +1,7 @@ -import { inject, injectable } from 'inversify'; -import { LibraryPackage } from '../../common/protocol/library-service'; +import { injectable, postConstruct, inject } from 'inversify'; +import { LibraryPackage, LibraryService } from '../../common/protocol/library-service'; import { ListWidget } from '../widgets/component-list/list-widget'; import { ListItemRenderer } from '../widgets/component-list/list-item-renderer'; -import { LibraryServiceProvider } from './library-service-provider'; @injectable() export class LibraryListWidget extends ListWidget { @@ -11,7 +10,7 @@ export class LibraryListWidget extends ListWidget { static WIDGET_LABEL = 'Library Manager'; constructor( - @inject(LibraryServiceProvider) protected service: LibraryServiceProvider, + @inject(LibraryService) protected service: LibraryService, @inject(ListItemRenderer) protected itemRenderer: ListItemRenderer) { super({ @@ -25,4 +24,13 @@ export class LibraryListWidget extends ListWidget { }); } + @postConstruct() + protected init(): void { + super.init(); + this.toDispose.pushAll([ + this.notificationCenter.onLibraryInstalled(() => this.refresh(undefined)), + this.notificationCenter.onLibraryUninstalled(() => this.refresh(undefined)), + ]); + } + } diff --git a/arduino-ide-extension/src/browser/library/library-service-provider.ts b/arduino-ide-extension/src/browser/library/library-service-provider.ts deleted file mode 100644 index c3e918c6..00000000 --- a/arduino-ide-extension/src/browser/library/library-service-provider.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { inject, injectable, postConstruct } from 'inversify'; -import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory'; -import { Event, Emitter } from '@theia/core/lib/common/event'; -import { DisposableCollection } from '@theia/core/lib/common/disposable'; -import { Searchable, InstalledEvent, UninstalledEvent } from '../../common/protocol'; -import { LibraryPackage, LibraryServiceServer, LibraryService } from '../../common/protocol/library-service'; - -@injectable() -export class LibraryServiceProvider implements Required { - - @inject(LibraryServiceServer) - protected readonly server: JsonRpcProxy; - - protected readonly onLibraryPackageInstalledEmitter = new Emitter>(); - protected readonly onLibraryPackageUninstalledEmitter = new Emitter>(); - protected readonly toDispose = new DisposableCollection( - this.onLibraryPackageInstalledEmitter, - this.onLibraryPackageUninstalledEmitter - ); - - @postConstruct() - protected init(): void { - this.server.setClient({ - notifyInstalled: event => this.onLibraryPackageInstalledEmitter.fire(event), - notifyUninstalled: event => this.onLibraryPackageUninstalledEmitter.fire(event) - }); - } - - get onLibraryPackageInstalled(): Event> { - return this.onLibraryPackageInstalledEmitter.event; - } - - get onLibraryPackageUninstalled(): Event> { - return this.onLibraryPackageUninstalledEmitter.event; - } - - // #region remote library service API - - async install(options: { item: LibraryPackage; version?: string | undefined; }): Promise { - return this.server.install(options); - } - - async list(options: LibraryService.List.Options): Promise { - return this.server.list(options); - } - - async uninstall(options: { item: LibraryPackage; }): Promise { - return this.server.uninstall(options); - } - - async search(options: Searchable.Options): Promise { - return this.server.search(options); - } - - // #endregion remote API - - dispose(): void { - this.toDispose.dispose(); - } - -} diff --git a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts index aace2c25..1c39e95b 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts @@ -4,11 +4,12 @@ import { Emitter, Event } from '@theia/core/lib/common/event'; import { MessageService } from '@theia/core/lib/common/message-service'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { MonitorService, MonitorConfig, MonitorError, Status, MonitorReadEvent } from '../../common/protocol/monitor-service'; -import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { Port, Board, BoardsService, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service'; import { MonitorServiceClientImpl } from './monitor-service-client-impl'; import { BoardsConfig } from '../boards/boards-config'; import { MonitorModel } from './monitor-model'; +import { NotificationCenter } from '../notification-center'; @injectable() export class MonitorConnection { @@ -25,8 +26,11 @@ export class MonitorConnection { @inject(BoardsService) protected readonly boardsService: BoardsService; - @inject(BoardsServiceClientImpl) - protected boardsServiceClient: BoardsServiceClientImpl; + @inject(BoardsServiceProvider) + protected readonly boardsServiceProvider: BoardsServiceProvider; + + @inject(NotificationCenter) + protected readonly notificationCenter: NotificationCenter; @inject(MessageService) protected messageService: MessageService; @@ -110,11 +114,11 @@ export class MonitorConnection { } } }); - this.boardsServiceClient.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this)); - this.boardsServiceClient.onAttachedBoardsChanged(event => { + this.boardsServiceProvider.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this)); + this.notificationCenter.onAttachedBoardsChanged(event => { if (this.autoConnect && this.connected) { - const { boardsConfig } = this.boardsServiceClient; - if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) { + const { boardsConfig } = this.boardsServiceProvider; + if (this.boardsServiceProvider.canUploadTo(boardsConfig, { silent: false })) { const { attached } = AttachedBoardsChangeEvent.diff(event); if (attached.boards.some(board => !!board.port && BoardsConfig.Config.sameAs(boardsConfig, board))) { const { selectedBoard: board, selectedPort: port } = boardsConfig; @@ -128,7 +132,7 @@ export class MonitorConnection { // Handles the `baudRate` changes by reconnecting if required. this.monitorModel.onChange(({ property }) => { if (property === 'baudRate' && this.autoConnect && this.connected) { - const { boardsConfig } = this.boardsServiceClient; + const { boardsConfig } = this.boardsServiceProvider; this.handleBoardConfigChange(boardsConfig); } }); @@ -154,7 +158,7 @@ export class MonitorConnection { // We have to make sure the previous boards config has been restored. // Otherwise, we might start the auto-connection without configured boards. this.applicationState.reachedState('started_contributions').then(() => { - const { boardsConfig } = this.boardsServiceClient; + const { boardsConfig } = this.boardsServiceProvider; this.handleBoardConfigChange(boardsConfig); }); } else if (oldValue && !value) { @@ -227,7 +231,7 @@ export class MonitorConnection { protected async handleBoardConfigChange(boardsConfig: BoardsConfig.Config): Promise { if (this.autoConnect) { - if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) { + if (this.boardsServiceProvider.canUploadTo(boardsConfig, { silent: false })) { // Instead of calling `getAttachedBoards` and filtering for `AttachedSerialBoard` we have to check the available ports. // The connected board might be unknown. See: https://github.com/arduino/arduino-pro-ide/issues/127#issuecomment-563251881 this.boardsService.getAvailablePorts().then(ports => { diff --git a/arduino-ide-extension/src/browser/monitor/monitor-model.ts b/arduino-ide-extension/src/browser/monitor/monitor-model.ts index 0ac6a27b..d42b5ec6 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-model.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-model.ts @@ -2,7 +2,7 @@ import { injectable, inject } from 'inversify'; import { Emitter, Event } from '@theia/core/lib/common/event'; import { MonitorConfig } from '../../common/protocol/monitor-service'; import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser'; -import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; @injectable() export class MonitorModel implements FrontendApplicationContribution { @@ -12,8 +12,8 @@ export class MonitorModel implements FrontendApplicationContribution { @inject(LocalStorageService) protected readonly localStorageService: LocalStorageService; - @inject(BoardsServiceClientImpl) - protected readonly boardsServiceClient: BoardsServiceClientImpl; + @inject(BoardsServiceProvider) + protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly onChangeEmitter: Emitter>; protected _autoscroll: boolean; diff --git a/arduino-ide-extension/src/browser/notification-center.ts b/arduino-ide-extension/src/browser/notification-center.ts new file mode 100644 index 00000000..97689a69 --- /dev/null +++ b/arduino-ide-extension/src/browser/notification-center.ts @@ -0,0 +1,92 @@ +import { inject, injectable, postConstruct } from 'inversify'; +import { Emitter } from '@theia/core/lib/common/event'; +import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; +import { NotificationServiceClient, NotificationServiceServer } from '../common/protocol/notification-service'; +import { AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config } from '../common/protocol'; + +@injectable() +export class NotificationCenter implements NotificationServiceClient, FrontendApplicationContribution { + + @inject(NotificationServiceServer) + protected readonly server: JsonRpcProxy; + + protected readonly indexUpdatedEmitter = new Emitter(); + protected readonly daemonStartedEmitter = new Emitter(); + protected readonly daemonStoppedEmitter = new Emitter(); + protected readonly configChangedEmitter = new Emitter<{ config: Config | undefined }>(); + protected readonly platformInstalledEmitter = new Emitter<{ item: BoardsPackage }>(); + protected readonly platformUninstalledEmitter = new Emitter<{ item: BoardsPackage }>(); + protected readonly libraryInstalledEmitter = new Emitter<{ item: LibraryPackage }>(); + protected readonly libraryUninstalledEmitter = new Emitter<{ item: LibraryPackage }>(); + protected readonly attachedBoardsChangedEmitter = new Emitter(); + + protected readonly toDispose = new DisposableCollection( + this.indexUpdatedEmitter, + this.daemonStartedEmitter, + this.daemonStoppedEmitter, + this.configChangedEmitter, + this.platformInstalledEmitter, + this.platformUninstalledEmitter, + this.libraryInstalledEmitter, + this.libraryUninstalledEmitter, + this.attachedBoardsChangedEmitter + ); + + readonly onIndexUpdated = this.indexUpdatedEmitter.event; + readonly onDaemonStarted = this.daemonStartedEmitter.event; + readonly onDaemonStopped = this.daemonStoppedEmitter.event; + readonly onConfigChanged = this.configChangedEmitter.event; + readonly onPlatformInstalled = this.platformInstalledEmitter.event; + readonly onPlatformUninstalled = this.platformUninstalledEmitter.event; + readonly onLibraryInstalled = this.libraryInstalledEmitter.event; + readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event; + readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event; + + @postConstruct() + protected init(): void { + this.server.setClient(this); + } + + onStop(): void { + this.toDispose.dispose(); + } + + notifyIndexUpdated(): void { + this.indexUpdatedEmitter.fire(); + } + + notifyDaemonStarted(): void { + this.daemonStartedEmitter.fire(); + } + + notifyDaemonStopped(): void { + this.daemonStoppedEmitter.fire(); + } + + notifyConfigChanged(event: { config: Config | undefined }): void { + this.configChangedEmitter.fire(event); + } + + notifyPlatformInstalled(event: { item: BoardsPackage }): void { + this.platformInstalledEmitter.fire(event); + } + + notifyPlatformUninstalled(event: { item: BoardsPackage }): void { + this.platformUninstalledEmitter.fire(event); + } + + notifyLibraryInstalled(event: { item: LibraryPackage }): void { + this.libraryInstalledEmitter.fire(event); + } + + notifyLibraryUninstalled(event: { item: LibraryPackage }): void { + this.libraryUninstalledEmitter.fire(event); + } + + notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { + this.attachedBoardsChangedEmitter.fire(event); + } + +} diff --git a/arduino-ide-extension/src/browser/tool-output/client-service-impl.ts b/arduino-ide-extension/src/browser/output-service-impl.ts similarity index 66% rename from arduino-ide-extension/src/browser/tool-output/client-service-impl.ts rename to arduino-ide-extension/src/browser/output-service-impl.ts index eb14bb41..ef2a2735 100644 --- a/arduino-ide-extension/src/browser/tool-output/client-service-impl.ts +++ b/arduino-ide-extension/src/browser/output-service-impl.ts @@ -1,10 +1,10 @@ -import { injectable, inject } from 'inversify'; +import { inject, injectable } from 'inversify'; import { OutputContribution } from '@theia/output/lib/browser/output-contribution'; import { OutputChannelManager, OutputChannelSeverity } from '@theia/output/lib/common/output-channel'; -import { ToolOutputServiceClient, ToolOutputMessage } from '../../common/protocol/tool-output-service'; +import { OutputService, OutputMessage } from '../common/protocol/output-service'; @injectable() -export class ToolOutputServiceClientImpl implements ToolOutputServiceClient { +export class OutputServiceImpl implements OutputService { @inject(OutputContribution) protected outputContribution: OutputContribution; @@ -12,13 +12,12 @@ export class ToolOutputServiceClientImpl implements ToolOutputServiceClient { @inject(OutputChannelManager) protected outputChannelManager: OutputChannelManager; - onMessageReceived(message: ToolOutputMessage): void { - const { tool, chunk } = message; - const name = `Arduino: ${tool}`; - const channel = this.outputChannelManager.getChannel(name); + append(message: OutputMessage): void { + const { name, chunk } = message; + const channel = this.outputChannelManager.getChannel(`Arduino: ${name}`); // Zen-mode: we do not reveal the output for daemon messages. - const show: Promise = tool === 'daemon' - // This will open and reveal the view but won't show it. You will see the toggle bottom panel on the status bar + const show: Promise = name === 'daemon' + // This will open and reveal the view but won't show it. You will see the toggle bottom panel on the status bar. ? this.outputContribution.openView({ activate: false, reveal: false }) // This will open, reveal but do not activate the Output view. : Promise.resolve(channel.show({ preserveFocus: true })); @@ -26,7 +25,7 @@ export class ToolOutputServiceClientImpl implements ToolOutputServiceClient { show.then(() => channel.append(chunk, this.toOutputSeverity(message))); } - private toOutputSeverity(message: ToolOutputMessage): OutputChannelSeverity { + protected toOutputSeverity(message: OutputMessage): OutputChannelSeverity { if (message.severity) { switch (message.severity) { case 'error': return OutputChannelSeverity.Error diff --git a/arduino-ide-extension/src/browser/theia/core/connection-status-service.ts b/arduino-ide-extension/src/browser/theia/core/connection-status-service.ts index eae61d39..079d7360 100644 --- a/arduino-ide-extension/src/browser/theia/core/connection-status-service.ts +++ b/arduino-ide-extension/src/browser/theia/core/connection-status-service.ts @@ -6,20 +6,30 @@ import { ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution, ConnectionStatus } from '@theia/core/lib/browser/connection-status-service'; -import { ArduinoDaemonClientImpl } from '../../arduino-daemon-client-impl'; +import { ArduinoDaemon } from '../../../common/protocol'; +import { NotificationCenter } from '../../notification-center'; @injectable() export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService { - @inject(ArduinoDaemonClientImpl) - protected readonly daemonClient: ArduinoDaemonClientImpl; + @inject(ArduinoDaemon) + protected readonly daemon: ArduinoDaemon; + + @inject(NotificationCenter) + protected readonly notificationCenter: NotificationCenter; + + protected isRunning = false; @postConstruct() - protected init(): void { + protected async init(): Promise { this.schedulePing(); + try { + this.isRunning = await this.daemon.isRunning(); + } catch { } + this.notificationCenter.onDaemonStarted(() => this.isRunning = true); + this.notificationCenter.onDaemonStopped(() => this.isRunning = false); this.wsConnectionProvider.onIncomingMessageActivity(() => { - // natural activity - this.updateStatus(this.daemonClient.isRunning); + this.updateStatus(this.isRunning); this.schedulePing(); }); } @@ -29,22 +39,35 @@ export class FrontendConnectionStatusService extends TheiaFrontendConnectionStat @injectable() export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution { - @inject(ArduinoDaemonClientImpl) - protected readonly daemonClient: ArduinoDaemonClientImpl; + @inject(ArduinoDaemon) + protected readonly daemon: ArduinoDaemon; + + @inject(NotificationCenter) + protected readonly notificationCenter: NotificationCenter; + + protected isRunning = false; + + @postConstruct() + protected async init(): Promise { + try { + this.isRunning = await this.daemon.isRunning(); + } catch { } + this.notificationCenter.onDaemonStarted(() => this.isRunning = true); + this.notificationCenter.onDaemonStopped(() => this.isRunning = false); + } protected onStateChange(state: ConnectionStatus): void { - if (!this.daemonClient.isRunning && state === ConnectionStatus.ONLINE) { + if (!this.isRunning && state === ConnectionStatus.ONLINE) { return; } super.onStateChange(state); } protected handleOffline(): void { - const { isRunning } = this.daemonClient; this.statusBar.setElement('connection-status', { alignment: StatusBarAlignment.LEFT, - text: isRunning ? 'Offline' : '$(bolt) CLI Daemon Offline', - tooltip: isRunning ? 'Cannot connect to the backend.' : 'Cannot connect to the CLI daemon.', + text: this.isRunning ? 'Offline' : '$(bolt) CLI Daemon Offline', + tooltip: this.isRunning ? 'Cannot connect to the backend.' : 'Cannot connect to the CLI daemon.', priority: 5000 }); this.toDisposeOnOnline.push(Disposable.create(() => this.statusBar.removeElement('connection-status'))); diff --git a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx index d2bc7a08..b302ff67 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx @@ -10,17 +10,13 @@ import { Searchable } from '../../../common/protocol/searchable'; import { ArduinoComponent } from '../../../common/protocol/arduino-component'; import { FilterableListContainer } from './filterable-list-container'; import { ListItemRenderer } from './list-item-renderer'; -import { CoreServiceClientImpl } from '../../core-service-client-impl'; -import { ArduinoDaemonClientImpl } from '../../arduino-daemon-client-impl'; +import { NotificationCenter } from '../../notification-center'; @injectable() export abstract class ListWidget extends ReactWidget { - @inject(CoreServiceClientImpl) - protected readonly coreServiceClient: CoreServiceClientImpl; - - @inject(ArduinoDaemonClientImpl) - protected readonly daemonClient: ArduinoDaemonClientImpl; + @inject(NotificationCenter) + protected readonly notificationCenter: NotificationCenter; /** * Do not touch or use it. It is for setting the focus on the `input` after the widget activation. @@ -49,9 +45,9 @@ export abstract class ListWidget extends ReactWidget protected init(): void { this.update(); this.toDispose.pushAll([ - this.coreServiceClient.onIndexUpdated(() => this.refresh(undefined)), - this.daemonClient.onDaemonStarted(() => this.refresh(undefined)), - this.daemonClient.onDaemonStopped(() => this.refresh(undefined)) + this.notificationCenter.onIndexUpdated(() => this.refresh(undefined)), + this.notificationCenter.onDaemonStarted(() => this.refresh(undefined)), + this.notificationCenter.onDaemonStopped(() => this.refresh(undefined)) ]); } diff --git a/arduino-ide-extension/src/common/protocol/arduino-daemon.ts b/arduino-ide-extension/src/common/protocol/arduino-daemon.ts index c1a5d63e..76662b05 100644 --- a/arduino-ide-extension/src/common/protocol/arduino-daemon.ts +++ b/arduino-ide-extension/src/common/protocol/arduino-daemon.ts @@ -1,13 +1,5 @@ -import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; - -export const ArduinoDaemonClient = Symbol('ArduinoDaemonClient'); -export interface ArduinoDaemonClient { - notifyStarted(): void; - notifyStopped(): void; -} - export const ArduinoDaemonPath = '/services/arduino-daemon'; export const ArduinoDaemon = Symbol('ArduinoDaemon'); -export interface ArduinoDaemon extends JsonRpcServer { +export interface ArduinoDaemon { isRunning(): Promise; } diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index 4d787284..e3bcf2e9 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -1,8 +1,7 @@ import { isWindows, isOSX } from '@theia/core/lib/common/os'; -import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; import { naturalCompare } from './../utils'; import { Searchable } from './searchable'; -import { Installable, InstallableClient } from './installable'; +import { Installable } from './installable'; import { ArduinoComponent } from './arduino-component'; export interface AttachedBoardsChangeEvent { @@ -11,6 +10,45 @@ export interface AttachedBoardsChangeEvent { } export namespace AttachedBoardsChangeEvent { + export function isEmpty(event: AttachedBoardsChangeEvent): boolean { + const { detached, attached } = diff(event); + return !!detached.boards.length && !!detached.ports.length && !!attached.boards.length && !!attached.ports.length; + } + + export function toString(event: AttachedBoardsChangeEvent): string { + let rows: string[] = []; + if (!isEmpty(event)) { + const { attached, detached } = diff(event); + const visitedAttachedPorts: Port[] = []; + const visitedDetachedPorts: Port[] = []; + for (const board of attached.boards) { + const port = board.port ? ` on ${Port.toString(board.port, { useLabel: true })}` : ''; + rows.push(` - Attached board: ${Board.toString(board)}${port}`); + if (board.port) { + visitedAttachedPorts.push(board.port); + } + } + for (const board of detached.boards) { + const port = board.port ? ` from ${Port.toString(board.port, { useLabel: true })}` : ''; + rows.push(` - Detached board: ${Board.toString(board)}${port}`); + if (board.port) { + visitedDetachedPorts.push(board.port); + } + } + for (const port of attached.ports) { + if (!visitedAttachedPorts.find(p => Port.sameAs(port, p))) { + rows.push(` - New port is available on ${Port.toString(port, { useLabel: true })}`); + } + } + for (const port of detached.ports) { + if (!visitedDetachedPorts.find(p => Port.sameAs(port, p))) { + rows.push(` - Port is no longer available on ${Port.toString(port, { useLabel: true })}`); + } + } + } + return rows.length ? rows.join('\n') : 'No changes.'; + } + export function diff(event: AttachedBoardsChangeEvent): Readonly<{ attached: { boards: Board[], @@ -45,14 +83,9 @@ export namespace AttachedBoardsChangeEvent { } -export const BoardsServiceClient = Symbol('BoardsServiceClient'); -export interface BoardsServiceClient extends InstallableClient { - notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void; -} - export const BoardsServicePath = '/services/boards-service'; export const BoardsService = Symbol('BoardsService'); -export interface BoardsService extends Installable, Searchable, JsonRpcServer { +export interface BoardsService extends Installable, Searchable { getAttachedBoards(): Promise; getAvailablePorts(): Promise; getBoardDetails(options: { fqbn: string }): Promise; @@ -185,9 +218,24 @@ export interface BoardsPackage extends ArduinoComponent { readonly boards: Board[]; } export namespace BoardsPackage { + export function equals(left: BoardsPackage, right: BoardsPackage): boolean { return left.id === right.id; } + + export function contains(selectedBoard: Board, { id, boards }: BoardsPackage): boolean { + if (boards.some(board => Board.sameAs(board, selectedBoard))) { + return true; + } + if (selectedBoard.fqbn) { + const [platform, architecture] = selectedBoard.fqbn.split(':'); + if (platform && architecture) { + return `${platform}:${architecture}` === id; + } + } + return false; + } + } export interface Board { diff --git a/arduino-ide-extension/src/common/protocol/config-service.ts b/arduino-ide-extension/src/common/protocol/config-service.ts index 65d84eab..0776eacd 100644 --- a/arduino-ide-extension/src/common/protocol/config-service.ts +++ b/arduino-ide-extension/src/common/protocol/config-service.ts @@ -1,14 +1,6 @@ -import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; - -export const ConfigServiceClient = Symbol('ConfigServiceClient'); -export interface ConfigServiceClient { - notifyConfigChanged(config: Config): void; - notifyInvalidConfig(): void; -} - export const ConfigServicePath = '/services/config-service'; export const ConfigService = Symbol('ConfigService'); -export interface ConfigService extends JsonRpcServer { +export interface ConfigService { getVersion(): Promise; getConfiguration(): Promise; getCliConfigFileUri(): Promise; diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index d244a77a..b8367b60 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -1,14 +1,8 @@ -import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; import { Programmer } from './boards-service'; -export const CoreServiceClient = Symbol('CoreServiceClient'); -export interface CoreServiceClient { - notifyIndexUpdated(): void; -} - export const CoreServicePath = '/services/core-service'; export const CoreService = Symbol('CoreService'); -export interface CoreService extends JsonRpcServer { +export interface CoreService { compile(options: CoreService.Compile.Options): Promise; upload(options: CoreService.Upload.Options): Promise; burnBootloader(options: CoreService.Bootloader.Options): Promise; diff --git a/arduino-ide-extension/src/common/protocol/index.ts b/arduino-ide-extension/src/common/protocol/index.ts index d21f2391..78219adf 100644 --- a/arduino-ide-extension/src/common/protocol/index.ts +++ b/arduino-ide-extension/src/common/protocol/index.ts @@ -9,6 +9,7 @@ export * from './library-service'; export * from './monitor-service'; export * from './searchable'; export * from './sketches-service'; -export * from './tool-output-service'; export * from './examples-service'; export * from './executable-service'; +export * from './output-service'; +export * from './notification-service'; diff --git a/arduino-ide-extension/src/common/protocol/installable.ts b/arduino-ide-extension/src/common/protocol/installable.ts index c7f3dee8..4bf90759 100644 --- a/arduino-ide-extension/src/common/protocol/installable.ts +++ b/arduino-ide-extension/src/common/protocol/installable.ts @@ -1,17 +1,6 @@ import { naturalCompare } from './../utils'; import { ArduinoComponent } from './arduino-component'; -export interface InstalledEvent { - readonly item: Readonly; -} -export interface UninstalledEvent { - readonly item: Readonly; -} -export interface InstallableClient { - notifyInstalled(event: InstalledEvent): void - notifyUninstalled(event: UninstalledEvent): void -} - export interface Installable { /** * If `options.version` is specified, that will be installed. Otherwise, `item.availableVersions[0]`. diff --git a/arduino-ide-extension/src/common/protocol/library-service.ts b/arduino-ide-extension/src/common/protocol/library-service.ts index 4909f606..04bb4ee6 100644 --- a/arduino-ide-extension/src/common/protocol/library-service.ts +++ b/arduino-ide-extension/src/common/protocol/library-service.ts @@ -1,22 +1,13 @@ -import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; import { Searchable } from './searchable'; +import { Installable } from './installable'; import { ArduinoComponent } from './arduino-component'; -import { Installable, InstallableClient } from './installable'; +export const LibraryServicePath = '/services/library-service'; +export const LibraryService = Symbol('LibraryService'); export interface LibraryService extends Installable, Searchable { - install(options: { item: LibraryPackage, version?: Installable.Version }): Promise; list(options: LibraryService.List.Options): Promise; } -export const LibraryServiceClient = Symbol('LibraryServiceClient'); -export interface LibraryServiceClient extends InstallableClient { -} - -export const LibraryServiceServerPath = '/services/library-service-server'; -export const LibraryServiceServer = Symbol('LibraryServiceServer'); -export interface LibraryServiceServer extends LibraryService, JsonRpcServer { -} - export namespace LibraryService { export namespace List { export interface Options { @@ -26,31 +17,38 @@ export namespace LibraryService { } export enum LibraryLocation { + /** * In the `libraries` subdirectory of the Arduino IDE installation. */ IDE_BUILTIN = 0, + /** * In the `libraries` subdirectory of the user directory (sketchbook). */ USER = 1, + /** * In the `libraries` subdirectory of a platform. */ PLATFORM_BUILTIN = 2, + /** * When `LibraryLocation` is used in a context where a board is specified, this indicates the library is in the `libraries` * subdirectory of a platform referenced by the board's platform. */ REFERENCED_PLATFORM_BUILTIN = 3 + } export interface LibraryPackage extends ArduinoComponent { + /** * Same as [`Library#real_name`](https://arduino.github.io/arduino-cli/latest/rpc/commands/#library). * Should be used for the UI, and `name` is used to uniquely identify a library. It does not have an ID. */ readonly label: string; + /** * An array of string that should be included into the `ino` file if this library is used. * For example, including `SD` will prepend `#include ` to the `ino` file. While including `Bridge` diff --git a/arduino-ide-extension/src/common/protocol/notification-service.ts b/arduino-ide-extension/src/common/protocol/notification-service.ts new file mode 100644 index 00000000..0129a1ca --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/notification-service.ts @@ -0,0 +1,22 @@ +import { LibraryPackage } from './library-service'; +import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; +import { BoardsPackage, AttachedBoardsChangeEvent } from './boards-service'; +import { Config } from './config-service'; + +export interface NotificationServiceClient { + notifyIndexUpdated(): void; + notifyDaemonStarted(): void; + notifyDaemonStopped(): void; + notifyConfigChanged(event: { config: Config | undefined }): void; + notifyPlatformInstalled(event: { item: BoardsPackage }): void; + notifyPlatformUninstalled(event: { item: BoardsPackage }): void; + notifyLibraryInstalled(event: { item: LibraryPackage }): void; + notifyLibraryUninstalled(event: { item: LibraryPackage }): void; + notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void; +} + +export const NotificationServicePath = '/services/notification-service'; +export const NotificationServiceServer = Symbol('NotificationServiceServer'); +export interface NotificationServiceServer extends Required, JsonRpcServer { + disposeClient(client: NotificationServiceClient): void; +} diff --git a/arduino-ide-extension/src/common/protocol/output-service.ts b/arduino-ide-extension/src/common/protocol/output-service.ts new file mode 100644 index 00000000..d948fafd --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/output-service.ts @@ -0,0 +1,11 @@ +export interface OutputMessage { + readonly name: string; + readonly chunk: string; + readonly severity?: 'error' | 'warning' | 'info'; +} + +export const OutputServicePath = '/services/output-service'; +export const OutputService = Symbol('OutputService'); +export interface OutputService { + append(message: OutputMessage): void; +} diff --git a/arduino-ide-extension/src/common/protocol/tool-output-service.ts b/arduino-ide-extension/src/common/protocol/tool-output-service.ts deleted file mode 100644 index 09376f19..00000000 --- a/arduino-ide-extension/src/common/protocol/tool-output-service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { JsonRpcServer } from '@theia/core'; - -export interface ToolOutputMessage { - readonly tool: string; - readonly chunk: string; - readonly severity?: 'error' | 'warning' | 'info'; -} - -export const ToolOutputServiceServer = Symbol('ToolOutputServiceServer'); -export interface ToolOutputServiceServer extends JsonRpcServer { - append(message: ToolOutputMessage): void; - disposeClient(client: ToolOutputServiceClient): void; -} - -export const ToolOutputServiceClient = Symbol('ToolOutputServiceClient'); -export interface ToolOutputServiceClient { - onMessageReceived(message: ToolOutputMessage): void; -} - -export namespace ToolOutputService { - export const SERVICE_PATH = '/tool-output-service'; -} diff --git a/arduino-ide-extension/src/node/arduino-daemon-impl.ts b/arduino-ide-extension/src/node/arduino-daemon-impl.ts index a4f59065..d7a4d881 100644 --- a/arduino-ide-extension/src/node/arduino-daemon-impl.ts +++ b/arduino-ide-extension/src/node/arduino-daemon-impl.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from '@theia/core/lib/common/event'; import { environment } from '@theia/application-package/lib/environment'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; -import { ArduinoDaemon, ArduinoDaemonClient, ToolOutputServiceServer } from '../common/protocol'; +import { ArduinoDaemon, NotificationServiceServer } from '../common/protocol'; import { DaemonLog } from './daemon-log'; import { CLI_CONFIG } from './cli-config'; import { getExecPath, spawnCommand } from './exec-util'; @@ -21,13 +21,12 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr @named('daemon') protected readonly logger: ILogger - @inject(ToolOutputServiceServer) - protected readonly toolOutputService: ToolOutputServiceServer; - @inject(EnvVariablesServer) protected readonly envVariablesServer: EnvVariablesServer; - protected readonly clients: Array = []; + @inject(NotificationServiceServer) + protected readonly notificationService: NotificationServiceServer; + protected readonly toDispose = new DisposableCollection(); protected readonly onDaemonStartedEmitter = new Emitter(); protected readonly onDaemonStoppedEmitter = new Emitter(); @@ -42,37 +41,6 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr this.startDaemon(); } - onStop(): void { - this.dispose(); - } - - // JSON-RPC proxy - - setClient(client: ArduinoDaemonClient | undefined): void { - if (client) { - if (this._running) { - client.notifyStarted() - } else { - client.notifyStopped(); - } - this.clients.push(client); - } - } - - dispose(): void { - this.toDispose.dispose(); - this.clients.length = 0; - } - - disposeClient(client: ArduinoDaemonClient): void { - const index = this.clients.indexOf(client); - if (index === -1) { - this.logger.warn('Could not dispose client. It was not registered or was already disposed.'); - } else { - this.clients.splice(index, 1); - } - } - // Daemon API async isRunning(): Promise { @@ -184,7 +152,7 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr if (code === 0 || signal === 'SIGINT' || signal === 'SIGKILL') { this.onData('Daemon has stopped.'); } else { - this.onData(`Daemon exited with ${typeof code === 'undefined' ? `signal '${signal}'` : `exit code: ${code}`}.`, { useOutput: false }); + this.onData(`Daemon exited with ${typeof code === 'undefined' ? `signal '${signal}'` : `exit code: ${code}`}.`); } }); daemon.on('error', error => { @@ -198,9 +166,7 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr this._running = true; this._ready.resolve(); this.onDaemonStartedEmitter.fire(); - for (const client of this.clients) { - client.notifyStarted(); - } + this.notificationService.notifyDaemonStarted(); } protected fireDaemonStopped(): void { @@ -211,15 +177,10 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr this._ready.reject(); // Reject all pending. this._ready = new Deferred(); this.onDaemonStoppedEmitter.fire(); - for (const client of this.clients) { - client.notifyStopped(); - } + this.notificationService.notifyDaemonStopped(); } - protected onData(message: string, options: { useOutput: boolean } = { useOutput: true }): void { - if (options.useOutput) { - this.toolOutputService.append({ tool: 'daemon', chunk: DaemonLog.toPrettyString(message) }); - } + protected onData(message: string): void { DaemonLog.log(this.logger, message); } diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 8564d438..419cbc17 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -5,130 +5,169 @@ import { ContainerModule } from 'inversify'; import { ArduinoDaemonImpl } from './arduino-daemon-impl'; import { ILogger } from '@theia/core/lib/common/logger'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; -import { LibraryServiceServerPath, LibraryServiceServer, LibraryServiceClient } from '../common/protocol/library-service'; -import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service'; -import { LibraryServiceServerImpl } from './library-service-server-impl'; +import { LibraryService, LibraryServicePath } from '../common/protocol/library-service'; +import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service'; +import { LibraryServiceImpl } from './library-service-server-impl'; import { BoardsServiceImpl } from './boards-service-impl'; import { CoreServiceImpl } from './core-service-impl'; -import { CoreService, CoreServicePath, CoreServiceClient } from '../common/protocol/core-service'; +import { CoreService, CoreServicePath } from '../common/protocol/core-service'; import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module'; import { CoreClientProvider } from './core-client-provider'; -import { ToolOutputService, ToolOutputServiceClient, ToolOutputServiceServer } from '../common/protocol/tool-output-service'; -import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core'; -import { ToolOutputServiceServerImpl } from './tool-output-service-impl'; -import { DefaultWorkspaceServerExt } from './default-workspace-server-ext'; -import { WorkspaceServer } from '@theia/workspace/lib/common'; +import { ConnectionHandler, JsonRpcConnectionHandler, JsonRpcProxy } from '@theia/core'; +import { DefaultWorkspaceServer } from './theia/workspace/default-workspace-server'; +import { WorkspaceServer as TheiaWorkspaceServer } from '@theia/workspace/lib/common'; import { SketchesServiceImpl } from './sketches-service-impl'; import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service'; -import { ConfigService, ConfigServicePath, ConfigServiceClient } from '../common/protocol/config-service'; -import { ArduinoDaemon, ArduinoDaemonPath, ArduinoDaemonClient } from '../common/protocol/arduino-daemon'; +import { ConfigService, ConfigServicePath } from '../common/protocol/config-service'; +import { ArduinoDaemon, ArduinoDaemonPath } from '../common/protocol/arduino-daemon'; import { MonitorServiceImpl } from './monitor/monitor-service-impl'; import { MonitorService, MonitorServicePath, MonitorServiceClient } from '../common/protocol/monitor-service'; import { MonitorClientProvider } from './monitor/monitor-client-provider'; import { ConfigServiceImpl } from './config-service-impl'; -import { ArduinoHostedPluginReader } from './arduino-plugin-reader'; -import { HostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader'; +import { HostedPluginReader } from './theia/plugin-ext/plugin-reader'; +import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader'; import { ConfigFileValidator } from './config-file-validator'; -import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; -import { ArduinoEnvVariablesServer } from './arduino-env-variables-server'; +import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { EnvVariablesServer } from './theia/env-variables/env-variables-server'; import { NodeFileSystemExt } from './node-filesystem-ext'; import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext'; import { ExamplesServiceImpl } from './examples-service-impl'; import { ExamplesService, ExamplesServicePath } from '../common/protocol/examples-service'; import { ExecutableService, ExecutableServicePath } from '../common/protocol/executable-service'; import { ExecutableServiceImpl } from './executable-service-impl'; +import { OutputServicePath, OutputService } from '../common/protocol/output-service'; +import { NotificationServiceServerImpl } from './notification-service-server'; +import { NotificationServiceServer, NotificationServiceClient, NotificationServicePath } from '../common/protocol'; export default new ContainerModule((bind, unbind, isBound, rebind) => { - rebind(EnvVariablesServer).to(ArduinoEnvVariablesServer).inSingletonScope(); - bind(ConfigFileValidator).toSelf().inSingletonScope(); - // XXX: The config service must start earlier than the daemon, hence the binding order does matter. // Shared config service + bind(ConfigFileValidator).toSelf().inSingletonScope(); bind(ConfigServiceImpl).toSelf().inSingletonScope(); bind(ConfigService).toService(ConfigServiceImpl); + // Note: The config service must start earlier than the daemon, hence the binding order of the BA contribution does matter. bind(BackendApplicationContribution).toService(ConfigServiceImpl); - bind(ConnectionHandler).toDynamicValue(context => - new JsonRpcConnectionHandler(ConfigServicePath, client => { - const server = context.container.get(ConfigServiceImpl); - server.setClient(client); - client.onDidCloseConnection(() => server.disposeClient(client)); - return server; - }) - ).inSingletonScope(); + bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(ConfigServicePath, () => context.container.get(ConfigService))).inSingletonScope(); // Shared daemon bind(ArduinoDaemonImpl).toSelf().inSingletonScope(); bind(ArduinoDaemon).toService(ArduinoDaemonImpl); bind(BackendApplicationContribution).toService(ArduinoDaemonImpl); - bind(ConnectionHandler).toDynamicValue(context => - new JsonRpcConnectionHandler(ArduinoDaemonPath, async client => { - const server = context.container.get(ArduinoDaemonImpl); - server.setClient(client); - client.onDidCloseConnection(() => server.disposeClient(client)); - return server; - }) - ).inSingletonScope(); + bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(ArduinoDaemonPath, () => context.container.get(ArduinoDaemon))).inSingletonScope(); - // Shared examples service - bind(ExamplesServiceImpl).toSelf().inSingletonScope(); - bind(ExamplesService).toService(ExamplesServiceImpl); - bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(ExamplesServicePath, () => context.container.get(ExamplesService))).inSingletonScope(); + // Examples service. One per backend, each connected FE gets a proxy. + bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => { + // const ExamplesServiceProxy = Symbol('ExamplesServiceProxy'); + // bind(ExamplesServiceProxy).toDynamicValue(ctx => new Proxy(ctx.container.get(ExamplesService), {})); + // bindBackendService(ExamplesServicePath, ExamplesServiceProxy); + bind(ExamplesServiceImpl).toSelf().inSingletonScope(); + bind(ExamplesService).toService(ExamplesServiceImpl); + bindBackendService(ExamplesServicePath, ExamplesService); + })); // Exposes the executable paths/URIs to the frontend bind(ExecutableServiceImpl).toSelf().inSingletonScope(); bind(ExecutableService).toService(ExecutableServiceImpl); bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(ExecutableServicePath, () => context.container.get(ExecutableService))).inSingletonScope(); - // Library service - bind(LibraryServiceServerImpl).toSelf().inSingletonScope(); - bind(LibraryServiceServer).toService(LibraryServiceServerImpl); - bind(ConnectionHandler).toDynamicValue(context => - new JsonRpcConnectionHandler(LibraryServiceServerPath, client => { - const server = context.container.get(LibraryServiceServerImpl); - server.setClient(client); - client.onDidCloseConnection(() => server.dispose()); - return server; - }) - ).inSingletonScope(); + // Library service. Singleton per backend, each connected FE gets its proxy. + bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => { + // const LibraryServiceProxy = Symbol('LibraryServiceProxy'); + // bind(LibraryServiceProxy).toDynamicValue(ctx => new Proxy(ctx.container.get(LibraryService), {})); + // bindBackendService(LibraryServicePath, LibraryServiceProxy); + bind(LibraryServiceImpl).toSelf().inSingletonScope(); + bind(LibraryService).toService(LibraryServiceImpl); + bindBackendService(LibraryServicePath, LibraryService); + })); // Shred sketches service bind(SketchesServiceImpl).toSelf().inSingletonScope(); bind(SketchesService).toService(SketchesServiceImpl); bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(SketchesServicePath, () => context.container.get(SketchesService))).inSingletonScope(); - // Boards service - const boardsServiceConnectionModule = ConnectionContainerModule.create(async ({ bind, bindBackendService }) => { + // Boards service. One singleton per backend that does the board and port polling. Each connected FE gets its proxy. + bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => { + // const BoardsServiceProxy = Symbol('BoardsServiceProxy'); + // bind(BoardsServiceProxy).toDynamicValue(ctx => new Proxy(ctx.container.get(BoardsService), {})); + // bindBackendService(BoardsServicePath, BoardsServiceProxy); bind(BoardsServiceImpl).toSelf().inSingletonScope(); bind(BoardsService).toService(BoardsServiceImpl); - bindBackendService(BoardsServicePath, BoardsService, (service, client) => { - service.setClient(client); + bindBackendService>(BoardsServicePath, BoardsService, (service, client) => { client.onDidCloseConnection(() => service.dispose()); return service; }); - }); - bind(ConnectionContainerModule).toConstantValue(boardsServiceConnectionModule); + })); // Shared Arduino core client provider service for the backend. bind(CoreClientProvider).toSelf().inSingletonScope(); - // Core service -> `verify` and `upload`. One per Theia connection. - const connectionConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { + // Core service -> `verify` and `upload`. Singleton per BE, each FE connection gets its proxy. + bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => { + // const CoreServiceProxy = Symbol('CoreServiceProxy'); + // bind(CoreServiceProxy).toDynamicValue(ctx => new Proxy(ctx.container.get(CoreService), {})); + // bindBackendService(CoreServicePath, CoreServiceProxy); bind(CoreServiceImpl).toSelf().inSingletonScope(); bind(CoreService).toService(CoreServiceImpl); - bindBackendService(BoardsServicePath, BoardsService); - bindBackendService(CoreServicePath, CoreService, (service, client) => { + bindBackendService(CoreServicePath, CoreService); + })); + + // #region Theia customizations + + bind(DefaultWorkspaceServer).toSelf().inSingletonScope(); + rebind(TheiaWorkspaceServer).toService(DefaultWorkspaceServer); + + bind(EnvVariablesServer).toSelf().inSingletonScope(); + rebind(TheiaEnvVariablesServer).toService(EnvVariablesServer); + + bind(HostedPluginReader).toSelf().inSingletonScope(); + rebind(TheiaHostedPluginReader).toService(HostedPluginReader); + + // #endregion Theia customizations + + // Shared monitor client provider service for the backend. + bind(MonitorClientProvider).toSelf().inSingletonScope(); + bind(MonitorServiceImpl).toSelf().inSingletonScope(); + bind(MonitorService).toService(MonitorServiceImpl); + bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => { + const MonitorServiceProxy = Symbol('MonitorServiceProxy'); + bind(MonitorServiceProxy).toDynamicValue(ctx => new Proxy(ctx.container.get(MonitorService), {})); + bindBackendService(MonitorServicePath, MonitorServiceProxy, (service, client) => { service.setClient(client); client.onDidCloseConnection(() => service.dispose()); return service; }); - }); - bind(ConnectionContainerModule).toConstantValue(connectionConnectionModule); + })); - // Tool output service -> feedback from the daemon, compile and flash - bind(ToolOutputServiceServerImpl).toSelf().inSingletonScope(); - bind(ToolOutputServiceServer).toService(ToolOutputServiceServerImpl); + // Set up cpp extension + if (!process.env.CPP_CLANGD_COMMAND) { + const segments = ['..', '..', 'build']; + if (os.platform() === 'win32') { + segments.push('clangd.exe'); + } else { + segments.push('bin'); + segments.push('clangd'); + } + const clangdCommand = join(__dirname, ...segments); + if (fs.existsSync(clangdCommand)) { + process.env.CPP_CLANGD_COMMAND = clangdCommand; + } + } + + // File-system extension for mapping paths to URIs + bind(NodeFileSystemExt).toSelf().inSingletonScope(); + bind(FileSystemExt).toService(NodeFileSystemExt); + bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(FileSystemExtPath, () => context.container.get(FileSystemExt))).inSingletonScope(); + + // Output service per connection. + bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bindFrontendService }) => { + bindFrontendService(OutputServicePath, OutputService); + })); + + // Notify all connected frontend instances + bind(NotificationServiceServerImpl).toSelf().inSingletonScope(); + bind(NotificationServiceServer).toService(NotificationServiceServerImpl); bind(ConnectionHandler).toDynamicValue(context => - new JsonRpcConnectionHandler(ToolOutputService.SERVICE_PATH, client => { - const server = context.container.get(ToolOutputServiceServer); + new JsonRpcConnectionHandler(NotificationServicePath, client => { + const server = context.container.get(NotificationServiceServer); server.setClient(client); client.onDidCloseConnection(() => server.disposeClient(client)); return server; @@ -153,52 +192,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { return parentLogger.child('config'); }).inSingletonScope().whenTargetNamed('config'); - // Default workspace server extension to initialize and use a fallback workspace. - // If nothing was set previously. - bind(DefaultWorkspaceServerExt).toSelf().inSingletonScope(); - rebind(WorkspaceServer).toService(DefaultWorkspaceServerExt); - - // Shared monitor client provider service for the backend. - bind(MonitorClientProvider).toSelf().inSingletonScope(); - - // Connection scoped service for the serial monitor. - const monitorServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { - bind(MonitorServiceImpl).toSelf().inSingletonScope(); - bind(MonitorService).toService(MonitorServiceImpl); - bindBackendService(MonitorServicePath, MonitorService, (service, client) => { - service.setClient(client); - client.onDidCloseConnection(() => service.dispose()); - return service; - }); - }); - bind(ConnectionContainerModule).toConstantValue(monitorServiceConnectionModule); - // Logger for the monitor service. bind(ILogger).toDynamicValue(ctx => { const parentLogger = ctx.container.get(ILogger); return parentLogger.child('monitor-service'); }).inSingletonScope().whenTargetNamed('monitor-service'); - // Set up cpp extension - if (!process.env.CPP_CLANGD_COMMAND) { - const segments = ['..', '..', 'build']; - if (os.platform() === 'win32') { - segments.push('clangd.exe'); - } else { - segments.push('bin'); - segments.push('clangd'); - } - const clangdCommand = join(__dirname, ...segments); - if (fs.existsSync(clangdCommand)) { - process.env.CPP_CLANGD_COMMAND = clangdCommand; - } - } - - bind(ArduinoHostedPluginReader).toSelf().inSingletonScope(); - rebind(HostedPluginReader).toService(ArduinoHostedPluginReader); - - // File-system extension for mapping paths to URIs - bind(NodeFileSystemExt).toSelf().inSingletonScope(); - bind(FileSystemExt).toService(NodeFileSystemExt); - bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(FileSystemExtPath, () => context.container.get(FileSystemExt))).inSingletonScope(); }); diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 7248ce6a..f5abee7c 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -1,19 +1,22 @@ import { injectable, inject, postConstruct, named } from 'inversify'; import { ILogger } from '@theia/core/lib/common/logger'; import { Deferred } from '@theia/core/lib/common/promise-util'; -import { BoardsService, BoardsPackage, Board, BoardsServiceClient, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer } from '../common/protocol'; +import { + BoardsService, + BoardsPackage, Board, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer, OutputService, NotificationServiceServer, AttachedBoardsChangeEvent +} from '../common/protocol'; import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp, Platform, PlatformUninstallResp, PlatformUninstallReq } from './cli-protocol/commands/core_pb'; import { CoreClientProvider } from './core-client-provider'; import { BoardListReq, BoardListResp, BoardDetailsReq, BoardDetailsResp } from './cli-protocol/commands/board_pb'; -import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; import { Installable } from '../common/protocol/installable'; import { ListProgrammersAvailableForUploadReq, ListProgrammersAvailableForUploadResp } from './cli-protocol/commands/upload_pb'; +import { Disposable } from '@theia/core/lib/common/disposable'; @injectable() -export class BoardsServiceImpl implements BoardsService { +export class BoardsServiceImpl implements BoardsService, Disposable { @inject(ILogger) protected logger: ILogger; @@ -25,8 +28,11 @@ export class BoardsServiceImpl implements BoardsService { @inject(CoreClientProvider) protected readonly coreClientProvider: CoreClientProvider; - @inject(ToolOutputServiceServer) - protected readonly toolOutputService: ToolOutputServiceServer; + @inject(OutputService) + protected readonly outputService: OutputService; + + @inject(NotificationServiceServer) + protected readonly notificationService: NotificationServiceServer; protected discoveryInitialized = false; protected discoveryTimer: NodeJS.Timer | undefined; @@ -38,7 +44,6 @@ export class BoardsServiceImpl implements BoardsService { protected attachedBoards: Board[] = []; protected availablePorts: Port[] = []; protected started = new Deferred(); - protected client: BoardsServiceClient | undefined; @postConstruct() protected async init(): Promise { @@ -49,19 +54,19 @@ export class BoardsServiceImpl implements BoardsService { const update = (oldBoards: Board[], newBoards: Board[], oldPorts: Port[], newPorts: Port[], message: string) => { this.attachedBoards = newBoards; this.availablePorts = newPorts; - this.discoveryLogger.info(`${message} - Discovered boards: ${JSON.stringify(newBoards)} and available ports: ${JSON.stringify(newPorts)}`); - if (this.client) { - this.client.notifyAttachedBoardsChanged({ - oldState: { - boards: oldBoards, - ports: oldPorts - }, - newState: { - boards: newBoards, - ports: newPorts - } - }); - } + const event = { + oldState: { + boards: oldBoards, + ports: oldPorts + }, + newState: { + boards: newBoards, + ports: newPorts + } + }; + this.discoveryLogger.info(`${message}`); + this.discoveryLogger.info(`${AttachedBoardsChangeEvent.toString(event)}`); + this.notificationService.notifyAttachedBoardsChanged(event); } const sortedBoards = boards.sort(Board.compare); const sortedPorts = ports.sort(Port.compare); @@ -103,16 +108,13 @@ export class BoardsServiceImpl implements BoardsService { }, 1000); } - setClient(client: BoardsServiceClient | undefined): void { - this.client = client; - } - dispose(): void { this.logger.info('>>> Disposing boards service...'); if (this.discoveryTimer !== undefined) { + this.logger.info('>>> Disposing the boards discovery...'); clearInterval(this.discoveryTimer); + this.logger.info('<<< Disposed the boards discovery.'); } - this.client = undefined; this.logger.info('<<< Disposed boards service.'); } @@ -213,14 +215,28 @@ export class BoardsServiceImpl implements BoardsService { const detailsReq = new BoardDetailsReq(); detailsReq.setInstance(instance); detailsReq.setFqbn(fqbn); - const detailsResp = await new Promise((resolve, reject) => client.boardDetails(detailsReq, (err, resp) => { + 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) { + resolve(undefined); + return; + } reject(err); return; } resolve(resp); })); + if (!detailsResp) { + return { + fqbn, + configOptions: [], + programmers: [], + requiredTools: [] + }; + } + const requiredTools = detailsResp.getToolsdependenciesList().map(t => { name: t.getName(), packager: t.getPackager(), @@ -391,18 +407,17 @@ export class BoardsServiceImpl implements BoardsService { resp.on('data', (r: PlatformInstallResp) => { const prog = r.getProgress(); if (prog && prog.getFile()) { - this.toolOutputService.append({ tool: 'board download', chunk: `downloading ${prog.getFile()}\n` }); + this.outputService.append({ name: 'board download', chunk: `downloading ${prog.getFile()}\n` }); } }); await new Promise((resolve, reject) => { resp.on('end', resolve); resp.on('error', reject); }); - if (this.client) { - const items = await this.search({}); - const updated = items.find(other => BoardsPackage.equals(other, item)) || item; - this.client.notifyInstalled({ item: updated }); - } + + 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); } @@ -426,7 +441,7 @@ export class BoardsServiceImpl implements BoardsService { const resp = client.platformUninstall(req); resp.on('data', (_: PlatformUninstallResp) => { if (!logged) { - this.toolOutputService.append({ tool: 'board uninstall', chunk: `uninstalling ${item.id}\n` }); + this.outputService.append({ name: 'board uninstall', chunk: `uninstalling ${item.id}\n` }); logged = true; } }) @@ -434,10 +449,9 @@ export class BoardsServiceImpl implements BoardsService { resp.on('end', resolve); resp.on('error', reject); }); - if (this.client) { - // Here, unlike at `install` we send out the argument `item`. Otherwise, we would not know about the board FQBN. - this.client.notifyUninstalled({ item }); - } + + // 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); } diff --git a/arduino-ide-extension/src/node/config-service-impl.ts b/arduino-ide-extension/src/node/config-service-impl.ts index 81d83409..a78f7d7f 100644 --- a/arduino-ide-extension/src/node/config-service-impl.ts +++ b/arduino-ide-extension/src/node/config-service-impl.ts @@ -8,7 +8,7 @@ import { ILogger } from '@theia/core/lib/common/logger'; import { FileUri } from '@theia/core/lib/node/file-uri'; import { Event, Emitter } from '@theia/core/lib/common/event'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; -import { ConfigService, Config, ConfigServiceClient } from '../common/protocol'; +import { ConfigService, Config, NotificationServiceServer } from '../common/protocol'; import * as fs from './fs-extra'; import { spawnCommand } from './exec-util'; import { RawData } from './cli-protocol/settings/settings_pb'; @@ -38,10 +38,12 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config @inject(ArduinoDaemonImpl) protected readonly daemon: ArduinoDaemonImpl; + @inject(NotificationServiceServer) + protected readonly notificationService: NotificationServiceServer; + protected updating = false; protected config: Config; protected cliConfig: DefaultCliConfig | undefined; - protected clients: Array = []; protected ready = new Deferred(); protected readonly configChangeEmitter = new Emitter(); @@ -94,25 +96,6 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config return this.getConfiguration().then(({ sketchDirUri }) => new URI(sketchDirUri).isEqualOrParent(new URI(uri))); } - setClient(client: ConfigServiceClient | undefined): void { - if (client) { - this.clients.push(client); - } - } - - dispose(): void { - this.clients.length = 0; - } - - disposeClient(client: ConfigServiceClient): void { - const index = this.clients.indexOf(client); - if (index === -1) { - this.logger.warn('Could not dispose client. It was not registered or was already disposed.'); - } else { - this.clients.splice(index, 1); - } - } - protected async loadCliConfig(): Promise { const cliConfigFileUri = await this.getCliConfigFileUri(); const cliConfigPath = FileUri.fsPath(cliConfigFileUri); @@ -214,9 +197,7 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config this.cliConfig = cliConfig; this.config = config; this.configChangeEmitter.fire(this.config); - for (const client of this.clients) { - client.notifyConfigChanged(this.config); - } + this.notificationService.notifyConfigChanged({ config: this.config }); }).finally(() => this.updating = false); } catch (err) { this.logger.error('Failed to update the daemon with the current CLI configuration.', err); @@ -227,15 +208,11 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config } protected fireConfigChanged(config: Config): void { - for (const client of this.clients) { - client.notifyConfigChanged(config); - } + this.notificationService.notifyConfigChanged({ config }); } protected fireInvalidConfig(): void { - for (const client of this.clients) { - client.notifyInvalidConfig(); - } + this.notificationService.notifyConfigChanged({ config: undefined }); } protected async unwatchCliConfig(): Promise { diff --git a/arduino-ide-extension/src/node/core-client-provider.ts b/arduino-ide-extension/src/node/core-client-provider.ts index 65ca90ac..4ce26785 100644 --- a/arduino-ide-extension/src/node/core-client-provider.ts +++ b/arduino-ide-extension/src/node/core-client-provider.ts @@ -1,26 +1,21 @@ import * as grpc from '@grpc/grpc-js'; import { inject, injectable } from 'inversify'; import { Event, Emitter } from '@theia/core/lib/common/event'; -import { ToolOutputServiceServer } from '../common/protocol'; import { GrpcClientProvider } from './grpc-client-provider'; import { ArduinoCoreClient } from './cli-protocol/commands/commands_grpc_pb'; import * as commandsGrpcPb from './cli-protocol/commands/commands_grpc_pb'; import { Instance } from './cli-protocol/commands/common_pb'; import { InitReq, InitResp, UpdateIndexReq, UpdateIndexResp, UpdateLibrariesIndexResp, UpdateLibrariesIndexReq } from './cli-protocol/commands/commands_pb'; +import { NotificationServiceServer } from '../common/protocol'; @injectable() export class CoreClientProvider extends GrpcClientProvider { - @inject(ToolOutputServiceServer) - protected readonly toolOutputService: ToolOutputServiceServer; + @inject(NotificationServiceServer) + protected readonly notificationService: NotificationServiceServer; - protected readonly onIndexUpdatedEmitter = new Emitter(); protected readonly onClientReadyEmitter = new Emitter(); - get onIndexUpdated(): Event { - return this.onIndexUpdatedEmitter.event; - } - get onClientReady(): Event { return this.onClientReadyEmitter.event; } @@ -76,11 +71,11 @@ export class CoreClientProvider extends GrpcClientProvider { - if (this.client) { - this.client.notifyIndexUpdated(); - } - }) - } + @inject(NotificationServiceServer) + protected readonly notificationService: NotificationServiceServer; async compile(options: CoreService.Compile.Options): Promise { - this.toolOutputService.append({ tool: 'compile', chunk: 'Compiling...\n' + JSON.stringify(options, null, 2) + '\n--------------------------\n' }); + this.outputService.append({ name: 'compile', chunk: 'Compiling...\n' + JSON.stringify(options, null, 2) + '\n--------------------------\n' }); const { sketchUri, fqbn } = options; const sketchFilePath = FileUri.fsPath(sketchUri); - const sketchpath = path.dirname(sketchFilePath); + const sketchpath = dirname(sketchFilePath); const coreClient = await this.coreClientProvider.client(); if (!coreClient) { @@ -61,25 +49,25 @@ export class CoreServiceImpl implements CoreService { try { await new Promise((resolve, reject) => { result.on('data', (cr: CompileResp) => { - this.toolOutputService.append({ tool: 'compile', chunk: Buffer.from(cr.getOutStream_asU8()).toString() }); - this.toolOutputService.append({ tool: 'compile', chunk: Buffer.from(cr.getErrStream_asU8()).toString() }); + this.outputService.append({ name: 'compile', chunk: Buffer.from(cr.getOutStream_asU8()).toString() }); + this.outputService.append({ name: 'compile', chunk: Buffer.from(cr.getErrStream_asU8()).toString() }); }); result.on('error', error => reject(error)); result.on('end', () => resolve()); }); - this.toolOutputService.append({ tool: 'compile', chunk: '\n--------------------------\nCompilation complete.\n' }); + this.outputService.append({ name: 'compile', chunk: '\n--------------------------\nCompilation complete.\n' }); } catch (e) { - this.toolOutputService.append({ tool: 'compile', chunk: `Compilation error: ${e}\n`, severity: 'error' }); + this.outputService.append({ name: 'compile', chunk: `Compilation error: ${e}\n`, severity: 'error' }); throw e; } } async upload(options: CoreService.Upload.Options): Promise { await this.compile(options); - this.toolOutputService.append({ tool: 'upload', chunk: 'Uploading...\n' + JSON.stringify(options, null, 2) + '\n--------------------------\n' }); + this.outputService.append({ name: 'upload', chunk: 'Uploading...\n' + JSON.stringify(options, null, 2) + '\n--------------------------\n' }); const { sketchUri, fqbn } = options; const sketchFilePath = FileUri.fsPath(sketchUri); - const sketchpath = path.dirname(sketchFilePath); + const sketchpath = dirname(sketchFilePath); const coreClient = await this.coreClientProvider.client(); if (!coreClient) { @@ -106,15 +94,15 @@ export class CoreServiceImpl implements CoreService { try { await new Promise((resolve, reject) => { result.on('data', (resp: UploadResp) => { - this.toolOutputService.append({ tool: 'upload', chunk: Buffer.from(resp.getOutStream_asU8()).toString() }); - this.toolOutputService.append({ tool: 'upload', chunk: Buffer.from(resp.getErrStream_asU8()).toString() }); + this.outputService.append({ name: 'upload', chunk: Buffer.from(resp.getOutStream_asU8()).toString() }); + this.outputService.append({ name: 'upload', chunk: Buffer.from(resp.getErrStream_asU8()).toString() }); }); result.on('error', error => reject(error)); result.on('end', () => resolve()); }); - this.toolOutputService.append({ tool: 'upload', chunk: '\n--------------------------\nUpload complete.\n' }); + this.outputService.append({ name: 'upload', chunk: '\n--------------------------\nUpload complete.\n' }); } catch (e) { - this.toolOutputService.append({ tool: 'upload', chunk: `Upload error: ${e}\n`, severity: 'error' }); + this.outputService.append({ name: 'upload', chunk: `Upload error: ${e}\n`, severity: 'error' }); throw e; } } @@ -141,24 +129,16 @@ export class CoreServiceImpl implements CoreService { try { await new Promise((resolve, reject) => { result.on('data', (resp: BurnBootloaderResp) => { - this.toolOutputService.append({ tool: 'bootloader', chunk: Buffer.from(resp.getOutStream_asU8()).toString() }); - this.toolOutputService.append({ tool: 'bootloader', chunk: Buffer.from(resp.getErrStream_asU8()).toString() }); + this.outputService.append({ name: 'bootloader', chunk: Buffer.from(resp.getOutStream_asU8()).toString() }); + this.outputService.append({ name: 'bootloader', chunk: Buffer.from(resp.getErrStream_asU8()).toString() }); }); result.on('error', error => reject(error)); result.on('end', () => resolve()); }); } catch (e) { - this.toolOutputService.append({ tool: 'bootloader', chunk: `Error while burning the bootloader: ${e}\n`, severity: 'error' }); + this.outputService.append({ name: 'bootloader', chunk: `Error while burning the bootloader: ${e}\n`, severity: 'error' }); throw e; } } - setClient(client: CoreServiceClient | undefined): void { - this.client = client; - } - - dispose(): void { - this.client = undefined; - } - } diff --git a/arduino-ide-extension/src/node/examples-service-impl.ts b/arduino-ide-extension/src/node/examples-service-impl.ts index 98c89a6c..0b604146 100644 --- a/arduino-ide-extension/src/node/examples-service-impl.ts +++ b/arduino-ide-extension/src/node/examples-service-impl.ts @@ -6,7 +6,7 @@ import { notEmpty } from '@theia/core/lib/common/objects'; import { Sketch } from '../common/protocol/sketches-service'; import { SketchesServiceImpl } from './sketches-service-impl'; import { ExamplesService, ExampleContainer } from '../common/protocol/examples-service'; -import { LibraryServiceServer, LibraryLocation, LibraryPackage } from '../common/protocol'; +import { LibraryLocation, LibraryPackage, LibraryService } from '../common/protocol'; import { ConfigServiceImpl } from './config-service-impl'; @injectable() @@ -15,8 +15,8 @@ export class ExamplesServiceImpl implements ExamplesService { @inject(SketchesServiceImpl) protected readonly sketchesService: SketchesServiceImpl; - @inject(LibraryServiceServer) - protected readonly libraryService: LibraryServiceServer; + @inject(LibraryService) + protected readonly libraryService: LibraryService; @inject(ConfigServiceImpl) protected readonly configService: ConfigServiceImpl; @@ -44,7 +44,7 @@ export class ExamplesServiceImpl implements ExamplesService { const current: ExampleContainer[] = []; const any: ExampleContainer[] = []; if (fqbn) { - const packages = await this.libraryService.list({ fqbn }); + const packages: LibraryPackage[] = await this.libraryService.list({ fqbn }); for (const pkg of packages) { const container = await this.tryGroupExamples(pkg); const { location } = pkg; diff --git a/arduino-ide-extension/src/node/library-service-server-impl.ts b/arduino-ide-extension/src/node/library-service-server-impl.ts index 8f6cae1b..00278b90 100644 --- a/arduino-ide-extension/src/node/library-service-server-impl.ts +++ b/arduino-ide-extension/src/node/library-service-server-impl.ts @@ -1,5 +1,5 @@ import { injectable, inject, postConstruct } from 'inversify'; -import { LibraryPackage, LibraryServiceClient, LibraryServiceServer } from '../common/protocol/library-service'; +import { LibraryPackage, LibraryService } from '../common/protocol/library-service'; import { CoreClientProvider } from './core-client-provider'; import { LibrarySearchReq, @@ -14,14 +14,14 @@ import { LibraryUninstallResp, Library } from './cli-protocol/commands/lib_pb'; -import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; import { Installable } from '../common/protocol/installable'; import { ILogger, notEmpty } from '@theia/core'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { FileUri } from '@theia/core/lib/node'; +import { OutputService, NotificationServiceServer } from '../common/protocol'; @injectable() -export class LibraryServiceServerImpl implements LibraryServiceServer { +export class LibraryServiceImpl implements LibraryService { @inject(ILogger) protected logger: ILogger; @@ -29,11 +29,13 @@ export class LibraryServiceServerImpl implements LibraryServiceServer { @inject(CoreClientProvider) protected readonly coreClientProvider: CoreClientProvider; - @inject(ToolOutputServiceServer) - protected readonly toolOutputService: ToolOutputServiceServer; + @inject(OutputService) + protected readonly outputService: OutputService; + + @inject(NotificationServiceServer) + protected readonly notificationServer: NotificationServiceServer; protected ready = new Deferred(); - protected client: LibraryServiceClient | undefined; @postConstruct() protected init(): void { @@ -108,7 +110,31 @@ export class LibraryServiceServerImpl implements LibraryServiceServer { req.setFqbn(fqbn); } - const resp = await new Promise((resolve, reject) => client.libraryList(req, ((error, resp) => !!error ? reject(error) : resolve(resp)))); + const resp = await new Promise((resolve, reject) => { + client.libraryList(req, ((error, r) => { + if (error) { + const { message } = error; + // Required core dependency is missing. + // https://github.com/arduino/arduino-cli/issues/954 + if (message.indexOf('missing platform release') !== -1 && message.indexOf('referenced by board') !== -1) { + resolve(undefined); + return; + } + // The core for the board is not installed, `lib list` cannot be filtered based on FQBN. + // https://github.com/arduino/arduino-cli/issues/955 + if (message.indexOf('platform') !== -1 && message.indexOf('is not installed') !== -1) { + resolve(undefined); + return; + } + reject(error); + return; + } + resolve(r); + })); + }); + if (!resp) { + return []; + } return resp.getInstalledLibraryList().map(item => { const library = item.getLibrary(); if (!library) { @@ -151,7 +177,7 @@ export class LibraryServiceServerImpl implements LibraryServiceServer { resp.on('data', (r: LibraryInstallResp) => { const prog = r.getProgress(); if (prog) { - this.toolOutputService.append({ tool: 'library', chunk: `downloading ${prog.getFile()}: ${prog.getCompleted()}%\n` }); + this.outputService.append({ name: 'library', chunk: `downloading ${prog.getFile()}: ${prog.getCompleted()}%\n` }); } }); await new Promise((resolve, reject) => { @@ -159,12 +185,9 @@ export class LibraryServiceServerImpl implements LibraryServiceServer { resp.on('error', reject); }); - if (this.client) { - const items = await this.search({}); - const updated = items.find(other => LibraryPackage.equals(other, item)) || item; - this.client.notifyInstalled({ item: updated }); - } - + const items = await this.search({}); + const updated = items.find(other => LibraryPackage.equals(other, item)) || item; + this.notificationServer.notifyLibraryInstalled({ item: updated }); console.info('<<< Library package installation done.', item); } @@ -186,7 +209,7 @@ export class LibraryServiceServerImpl implements LibraryServiceServer { const resp = client.libraryUninstall(req); resp.on('data', (_: LibraryUninstallResp) => { if (!logged) { - this.toolOutputService.append({ tool: 'library', chunk: `uninstalling ${item.name}:${item.installedVersion}%\n` }); + this.outputService.append({ name: 'library', chunk: `uninstalling ${item.name}:${item.installedVersion}%\n` }); logged = true; } }); @@ -194,19 +217,13 @@ export class LibraryServiceServerImpl implements LibraryServiceServer { resp.on('end', resolve); resp.on('error', reject); }); - if (this.client) { - this.client.notifyUninstalled({ item }); - } - console.info('<<< Library package uninstallation done.', item); - } - setClient(client: LibraryServiceClient | undefined): void { - this.client = client; + this.notificationServer.notifyLibraryUninstalled({ item }); + console.info('<<< Library package uninstallation done.', item); } dispose(): void { this.logger.info('>>> Disposing library service...'); - this.client = undefined; this.logger.info('<<< Disposed library service.'); } diff --git a/arduino-ide-extension/src/node/notification-service-server.ts b/arduino-ide-extension/src/node/notification-service-server.ts new file mode 100644 index 00000000..65f7a652 --- /dev/null +++ b/arduino-ide-extension/src/node/notification-service-server.ts @@ -0,0 +1,65 @@ +import { injectable } from 'inversify'; +import { NotificationServiceServer, NotificationServiceClient, AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config } from '../common/protocol'; + +@injectable() +export class NotificationServiceServerImpl implements NotificationServiceServer { + + protected readonly clients: NotificationServiceClient[] = []; + + notifyIndexUpdated(): void { + this.clients.forEach(client => client.notifyIndexUpdated()); + } + + notifyDaemonStarted(): void { + this.clients.forEach(client => client.notifyDaemonStarted()); + } + + notifyDaemonStopped(): void { + this.clients.forEach(client => client.notifyDaemonStopped()); + } + + notifyPlatformInstalled(event: { item: BoardsPackage }): void { + this.clients.forEach(client => client.notifyPlatformInstalled(event)); + } + + notifyPlatformUninstalled(event: { item: BoardsPackage }): void { + this.clients.forEach(client => client.notifyPlatformUninstalled(event)); + } + + notifyLibraryInstalled(event: { item: LibraryPackage }): void { + this.clients.forEach(client => client.notifyLibraryInstalled(event)); + } + + notifyLibraryUninstalled(event: { item: LibraryPackage }): void { + this.clients.forEach(client => client.notifyLibraryUninstalled(event)); + } + + notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { + this.clients.forEach(client => client.notifyAttachedBoardsChanged(event)); + } + + notifyConfigChanged(event: { config: Config | undefined }): void { + this.clients.forEach(client => client.notifyConfigChanged(event)); + } + + setClient(client: NotificationServiceClient): void { + this.clients.push(client); + } + + disposeClient(client: NotificationServiceClient): void { + const index = this.clients.indexOf(client); + if (index === -1) { + console.warn(`Could not dispose notification service client. It was not registered.`); + return; + } + this.clients.splice(index, 1); + } + + dispose(): void { + for (const client of this.clients) { + this.disposeClient(client); + } + this.clients.length = 0; + } + +} diff --git a/arduino-ide-extension/src/node/arduino-env-variables-server.ts b/arduino-ide-extension/src/node/theia/env-variables/env-variables-server.ts similarity index 68% rename from arduino-ide-extension/src/node/arduino-env-variables-server.ts rename to arduino-ide-extension/src/node/theia/env-variables/env-variables-server.ts index 2a0084e6..314225c1 100644 --- a/arduino-ide-extension/src/node/arduino-env-variables-server.ts +++ b/arduino-ide-extension/src/node/theia/env-variables/env-variables-server.ts @@ -2,11 +2,11 @@ import { join } from 'path'; import { homedir } from 'os'; import { injectable } from 'inversify'; import { FileUri } from '@theia/core/lib/node/file-uri'; -import { EnvVariablesServerImpl } from '@theia/core/lib/node/env-variables/env-variables-server'; import { BackendApplicationConfigProvider } from '@theia/core/lib/node/backend-application-config-provider'; +import { EnvVariablesServerImpl as TheiaEnvVariablesServerImpl } from '@theia/core/lib/node/env-variables/env-variables-server'; @injectable() -export class ArduinoEnvVariablesServer extends EnvVariablesServerImpl { +export class EnvVariablesServer extends TheiaEnvVariablesServerImpl { protected readonly configDirUri = Promise.resolve(FileUri.create(join(homedir(), BackendApplicationConfigProvider.get().configDirName)).toString()); diff --git a/arduino-ide-extension/src/node/arduino-plugin-reader.ts b/arduino-ide-extension/src/node/theia/plugin-ext/plugin-reader.ts similarity index 86% rename from arduino-ide-extension/src/node/arduino-plugin-reader.ts rename to arduino-ide-extension/src/node/theia/plugin-ext/plugin-reader.ts index 7a0f995d..b8669dc8 100644 --- a/arduino-ide-extension/src/node/arduino-plugin-reader.ts +++ b/arduino-ide-extension/src/node/theia/plugin-ext/plugin-reader.ts @@ -1,11 +1,11 @@ import { injectable, inject } from 'inversify'; -import { HostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader'; +import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader'; import { PluginPackage, PluginContribution } from '@theia/plugin-ext/lib/common/plugin-protocol'; -import { CLI_CONFIG } from './cli-config'; -import { ConfigServiceImpl } from './config-service-impl'; +import { CLI_CONFIG } from '../../cli-config'; +import { ConfigServiceImpl } from '../../config-service-impl'; @injectable() -export class ArduinoHostedPluginReader extends HostedPluginReader { +export class HostedPluginReader extends TheiaHostedPluginReader { @inject(ConfigServiceImpl) protected readonly configService: ConfigServiceImpl; diff --git a/arduino-ide-extension/src/node/default-workspace-server-ext.ts b/arduino-ide-extension/src/node/theia/workspace/default-workspace-server.ts similarity index 64% rename from arduino-ide-extension/src/node/default-workspace-server-ext.ts rename to arduino-ide-extension/src/node/theia/workspace/default-workspace-server.ts index cc232a6f..b25cb0d0 100644 --- a/arduino-ide-extension/src/node/default-workspace-server-ext.ts +++ b/arduino-ide-extension/src/node/theia/workspace/default-workspace-server.ts @@ -1,10 +1,10 @@ import { injectable, inject } from 'inversify'; -import { ILogger } from '@theia/core'; -import { DefaultWorkspaceServer } from '@theia/workspace/lib/node/default-workspace-server'; -import { ConfigService } from '../common/protocol/config-service'; +import { ILogger } from '@theia/core/lib/common/logger'; +import { DefaultWorkspaceServer as TheiaDefaultWorkspaceServer } from '@theia/workspace/lib/node/default-workspace-server'; +import { ConfigService } from '../../../common/protocol/config-service'; @injectable() -export class DefaultWorkspaceServerExt extends DefaultWorkspaceServer { +export class DefaultWorkspaceServer extends TheiaDefaultWorkspaceServer { @inject(ConfigService) protected readonly configService: ConfigService; diff --git a/arduino-ide-extension/src/node/tool-output-service-impl.ts b/arduino-ide-extension/src/node/tool-output-service-impl.ts deleted file mode 100644 index 7edd1ff7..00000000 --- a/arduino-ide-extension/src/node/tool-output-service-impl.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { injectable } from 'inversify'; -import { ToolOutputServiceServer, ToolOutputServiceClient, ToolOutputMessage } from '../common/protocol/tool-output-service'; - -@injectable() -export class ToolOutputServiceServerImpl implements ToolOutputServiceServer { - protected clients: ToolOutputServiceClient[] = []; - - append(message: ToolOutputMessage): void { - if (!message.chunk) { - return; - } - for (const client of this.clients) { - client.onMessageReceived(message); - } - } - - setClient(client: ToolOutputServiceClient | undefined): void { - if (!client) { - return; - } - this.clients.push(client); - } - - disposeClient(client: ToolOutputServiceClient): void { - const index = this.clients.indexOf(client); - if (index === -1) { - console.warn(`Could not dispose tools output client. It was not registered.`); - return; - } - this.clients.splice(index, 1); - } - - dispose(): void { - for (const client of this.clients) { - this.disposeClient(client); - } - this.clients.length = 0; - } - -} diff --git a/arduino-ide-extension/src/test/browser/boards-service-client-impl.test.ts b/arduino-ide-extension/src/test/browser/boards-service-client-impl.test.ts_back similarity index 96% rename from arduino-ide-extension/src/test/browser/boards-service-client-impl.test.ts rename to arduino-ide-extension/src/test/browser/boards-service-client-impl.test.ts_back index b4f68e4b..e5f5e2d4 100644 --- a/arduino-ide-extension/src/test/browser/boards-service-client-impl.test.ts +++ b/arduino-ide-extension/src/test/browser/boards-service-client-impl.test.ts_back @@ -11,9 +11,10 @@ import { MessageClient } from '@theia/core/lib/common/message-service-protocol'; import { MessageService } from '@theia/core/lib/common/message-service'; import { StorageService } from '@theia/core/lib/browser/storage-service'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; -import { BoardsService, Board, Port, BoardsPackage, BoardDetails, BoardsServiceClient } from '../../common/protocol'; -import { BoardsServiceClientImpl, AvailableBoard } from '../../browser/boards/boards-service-client-impl'; +import { BoardsService, Board, Port, BoardsPackage, BoardDetails } from '../../common/protocol'; +import { BoardsServiceProvider, AvailableBoard } from '../../browser/boards/boards-service-provider'; import { BoardsConfig } from '../../browser/boards/boards-config'; +import { NotificationCenter } from '../../browser/notification-center'; // tslint:disable: no-unused-expression @@ -33,14 +34,14 @@ describe('boards-service-client-impl', () => { let stub: sinon.SinonStub; let server: MockBoardsService; - let client: BoardsServiceClientImpl; + let client: BoardsServiceProvider; + let notificationCenter: NotificationCenter; beforeEach(() => { stub = sinon.stub(os, 'isOSX').value(true); const container = init(); server = container.get(MockBoardsService); - client = container.get(BoardsServiceClientImpl); - server.setClient(client); + client = container.get(BoardsServiceProvider); }); afterEach(() => { @@ -165,7 +166,7 @@ function init(): Container { container.bind(ILogger).toService(MockLogger); container.bind(MockStorageService).toSelf(); container.bind(StorageService).toService(MockStorageService); - container.bind(BoardsServiceClientImpl).toSelf(); + container.bind(BoardsServiceProvider).toSelf(); container.bind(MessageClient).toSelf().inSingletonScope(); container.bind(MessageService).toSelf().inSingletonScope(); return container; @@ -293,10 +294,6 @@ export class MockBoardsService implements BoardsService { this.client = undefined; } - setClient(client: BoardsServiceClient | undefined): void { - this.client = client; - } - } @injectable()