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 c312a144f..44fde3c4d 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -163,7 +163,7 @@ import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service import { ResponseServiceImpl } from './response-service-impl'; import { ResponseService, - ResponseServiceArduino, + ResponseServiceClient, ResponseServicePath, } from '../common/protocol/response-service'; import { NotificationCenter } from './notification-center'; @@ -302,6 +302,8 @@ import { CompilerErrors } from './contributions/compiler-errors'; import { WidgetManager } from './theia/core/widget-manager'; import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager'; import { StartupTask } from './widgets/sketchbook/startup-task'; +import { IndexesUpdateProgress } from './contributions/indexes-update-progress'; +import { Daemon } from './contributions/daemon'; MonacoThemingService.register({ id: 'arduino-theme', @@ -695,6 +697,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { Contribution.configure(bind, Format); Contribution.configure(bind, CompilerErrors); Contribution.configure(bind, StartupTask); + Contribution.configure(bind, IndexesUpdateProgress); + Contribution.configure(bind, Daemon); // Disabled the quick-pick customization from Theia when multiple formatters are available. // Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors. @@ -716,7 +720,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { }); bind(ResponseService).toService(ResponseServiceImpl); - bind(ResponseServiceArduino).toService(ResponseServiceImpl); + bind(ResponseServiceClient).toService(ResponseServiceImpl); bind(NotificationCenter).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(NotificationCenter); 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 62d9c5534..88adcf0f0 100644 --- a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts +++ b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts @@ -8,7 +8,7 @@ import { Port, } from '../../common/protocol/boards-service'; import { BoardsServiceProvider } from './boards-service-provider'; -import { Installable, ResponseServiceArduino } from '../../common/protocol'; +import { Installable, ResponseServiceClient } from '../../common/protocol'; import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution'; import { nls } from '@theia/core/lib/common'; import { NotificationCenter } from '../notification-center'; @@ -45,8 +45,8 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution { @inject(BoardsServiceProvider) protected readonly boardsServiceClient: BoardsServiceProvider; - @inject(ResponseServiceArduino) - protected readonly responseService: ResponseServiceArduino; + @inject(ResponseServiceClient) + protected readonly responseService: ResponseServiceClient; @inject(BoardsListWidgetFrontendContribution) protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution; @@ -86,7 +86,7 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution { // installed, though this is not strictly necessary. It's more of a // cleanup, to ensure the related variables are representative of // current state. - this.notificationCenter.onPlatformInstalled((installed) => { + this.notificationCenter.onPlatformDidInstall((installed) => { if (this.lastRefusedPackageId === installed.item.id) { this.clearLastRefusedPromptInfo(); } diff --git a/arduino-ide-extension/src/browser/boards/boards-config.tsx b/arduino-ide-extension/src/browser/boards/boards-config.tsx index 1a80ced5d..4449c5cf1 100644 --- a/arduino-ide-extension/src/browser/boards/boards-config.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-config.tsx @@ -113,7 +113,7 @@ export class BoardsConfig extends React.Component< ); } }), - this.props.notificationCenter.onAttachedBoardsChanged((event) => + this.props.notificationCenter.onAttachedBoardsDidChange((event) => this.updatePorts( event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports @@ -126,19 +126,19 @@ export class BoardsConfig extends React.Component< ); } ), - this.props.notificationCenter.onPlatformInstalled(() => + this.props.notificationCenter.onPlatformDidInstall(() => this.updateBoards(this.state.query) ), - this.props.notificationCenter.onPlatformUninstalled(() => + this.props.notificationCenter.onPlatformDidUninstall(() => this.updateBoards(this.state.query) ), - this.props.notificationCenter.onIndexUpdated(() => + this.props.notificationCenter.onIndexDidUpdate(() => this.updateBoards(this.state.query) ), - this.props.notificationCenter.onDaemonStarted(() => + this.props.notificationCenter.onDaemonDidStart(() => this.updateBoards(this.state.query) ), - this.props.notificationCenter.onDaemonStopped(() => + this.props.notificationCenter.onDaemonDidStop(() => this.setState({ searchResults: [] }) ), this.props.onFilteredTextDidChangeEvent((query) => 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 63255656a..30f2f7781 100644 --- a/arduino-ide-extension/src/browser/boards/boards-data-store.ts +++ b/arduino-ide-extension/src/browser/boards/boards-data-store.ts @@ -33,7 +33,7 @@ export class BoardsDataStore implements FrontendApplicationContribution { protected readonly onChangedEmitter = new Emitter(); onStart(): void { - this.notificationCenter.onPlatformInstalled(async ({ item }) => { + this.notificationCenter.onPlatformDidInstall(async ({ item }) => { let shouldFireChanged = false; for (const fqbn of item.boards .map(({ fqbn }) => fqbn) 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 ca2508fb9..a05c85685 100644 --- a/arduino-ide-extension/src/browser/boards/boards-list-widget.ts +++ b/arduino-ide-extension/src/browser/boards/boards-list-widget.ts @@ -33,10 +33,10 @@ export class BoardsListWidget extends ListWidget { protected override init(): void { super.init(); this.toDispose.pushAll([ - this.notificationCenter.onPlatformInstalled(() => + this.notificationCenter.onPlatformDidInstall(() => this.refresh(undefined) ), - this.notificationCenter.onPlatformUninstalled(() => + this.notificationCenter.onPlatformDidUninstall(() => this.refresh(undefined) ), ]); diff --git a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts index 98bd4fa14..b7db7b4c6 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts @@ -77,13 +77,13 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { private readonly _reconciled = new Deferred(); onStart(): void { - this.notificationCenter.onAttachedBoardsChanged( + this.notificationCenter.onAttachedBoardsDidChange( this.notifyAttachedBoardsChanged.bind(this) ); - this.notificationCenter.onPlatformInstalled( + this.notificationCenter.onPlatformDidInstall( this.notifyPlatformInstalled.bind(this) ); - this.notificationCenter.onPlatformUninstalled( + this.notificationCenter.onPlatformDidUninstall( this.notifyPlatformUninstalled.bind(this) ); diff --git a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts index 927af4868..8f5f32eb4 100644 --- a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts +++ b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts @@ -7,7 +7,7 @@ import { ArduinoMenus } from '../menu/arduino-menus'; import { Installable, LibraryService, - ResponseServiceArduino, + ResponseServiceClient, } from '../../common/protocol'; import { SketchContribution, @@ -22,8 +22,8 @@ export class AddZipLibrary extends SketchContribution { @inject(EnvVariablesServer) protected readonly envVariableServer: EnvVariablesServer; - @inject(ResponseServiceArduino) - protected readonly responseService: ResponseServiceArduino; + @inject(ResponseServiceClient) + protected readonly responseService: ResponseServiceClient; @inject(LibraryService) protected readonly libraryService: LibraryService; diff --git a/arduino-ide-extension/src/browser/contributions/board-selection.ts b/arduino-ide-extension/src/browser/contributions/board-selection.ts index 16b025662..24527ca79 100644 --- a/arduino-ide-extension/src/browser/contributions/board-selection.ts +++ b/arduino-ide-extension/src/browser/contributions/board-selection.ts @@ -101,8 +101,8 @@ PID: ${PID}`; } override onStart(): void { - this.notificationCenter.onPlatformInstalled(() => this.updateMenus()); - this.notificationCenter.onPlatformUninstalled(() => this.updateMenus()); + this.notificationCenter.onPlatformDidInstall(() => this.updateMenus()); + this.notificationCenter.onPlatformDidUninstall(() => this.updateMenus()); this.boardsServiceProvider.onBoardsConfigChanged(() => this.updateMenus()); this.boardsServiceProvider.onAvailableBoardsChanged(() => this.updateMenus() diff --git a/arduino-ide-extension/src/browser/contributions/daemon.ts b/arduino-ide-extension/src/browser/contributions/daemon.ts new file mode 100644 index 000000000..740dcccf7 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/daemon.ts @@ -0,0 +1,41 @@ +import { nls } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ArduinoDaemon } from '../../common/protocol'; +import { Contribution, Command, CommandRegistry } from './contribution'; + +@injectable() +export class Daemon extends Contribution { + @inject(ArduinoDaemon) + private readonly daemon: ArduinoDaemon; + + override registerCommands(registry: CommandRegistry): void { + registry.registerCommand(Daemon.Commands.START_DAEMON, { + execute: () => this.daemon.start(), + }); + registry.registerCommand(Daemon.Commands.STOP_DAEMON, { + execute: () => this.daemon.stop(), + }); + registry.registerCommand(Daemon.Commands.RESTART_DAEMON, { + execute: () => this.daemon.restart(), + }); + } +} +export namespace Daemon { + export namespace Commands { + export const START_DAEMON: Command = { + id: 'arduino-start-daemon', + label: nls.localize('arduino/daemon/start', 'Start Daemon'), + category: 'Arduino', + }; + export const STOP_DAEMON: Command = { + id: 'arduino-stop-daemon', + label: nls.localize('arduino/daemon/stop', 'Stop Daemon'), + category: 'Arduino', + }; + export const RESTART_DAEMON: Command = { + id: 'arduino-restart-daemon', + label: nls.localize('arduino/daemon/restart', 'Restart Daemon'), + category: 'Arduino', + }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/debug.ts b/arduino-ide-extension/src/browser/contributions/debug.ts index b1550ce32..8d94df4dd 100644 --- a/arduino-ide-extension/src/browser/contributions/debug.ts +++ b/arduino-ide-extension/src/browser/contributions/debug.ts @@ -83,8 +83,8 @@ export class Debug extends SketchContribution { this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) => this.refreshState(selectedBoard) ); - this.notificationCenter.onPlatformInstalled(() => this.refreshState()); - this.notificationCenter.onPlatformUninstalled(() => this.refreshState()); + this.notificationCenter.onPlatformDidInstall(() => this.refreshState()); + this.notificationCenter.onPlatformDidUninstall(() => this.refreshState()); } override onReady(): MaybePromise { diff --git a/arduino-ide-extension/src/browser/contributions/examples.ts b/arduino-ide-extension/src/browser/contributions/examples.ts index 17368feab..d7185178b 100644 --- a/arduino-ide-extension/src/browser/contributions/examples.ts +++ b/arduino-ide-extension/src/browser/contributions/examples.ts @@ -202,8 +202,8 @@ export class LibraryExamples extends Examples { protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); override onStart(): void { - this.notificationCenter.onLibraryInstalled(() => this.register()); - this.notificationCenter.onLibraryUninstalled(() => this.register()); + this.notificationCenter.onLibraryDidInstall(() => this.register()); + this.notificationCenter.onLibraryDidUninstall(() => this.register()); } override async onReady(): Promise { diff --git a/arduino-ide-extension/src/browser/contributions/include-library.ts b/arduino-ide-extension/src/browser/contributions/include-library.ts index 7347c3fa9..19853c7ff 100644 --- a/arduino-ide-extension/src/browser/contributions/include-library.ts +++ b/arduino-ide-extension/src/browser/contributions/include-library.ts @@ -49,8 +49,8 @@ export class IncludeLibrary extends SketchContribution { this.boardsServiceClient.onBoardsConfigChanged(() => this.updateMenuActions() ); - this.notificationCenter.onLibraryInstalled(() => this.updateMenuActions()); - this.notificationCenter.onLibraryUninstalled(() => + this.notificationCenter.onLibraryDidInstall(() => this.updateMenuActions()); + this.notificationCenter.onLibraryDidUninstall(() => this.updateMenuActions() ); } diff --git a/arduino-ide-extension/src/browser/contributions/indexes-update-progress.ts b/arduino-ide-extension/src/browser/contributions/indexes-update-progress.ts new file mode 100644 index 000000000..d8762b841 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/indexes-update-progress.ts @@ -0,0 +1,71 @@ +import { Progress } from '@theia/core/lib/common/message-service-protocol'; +import { ProgressService } from '@theia/core/lib/common/progress-service'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ProgressMessage } from '../../common/protocol'; +import { NotificationCenter } from '../notification-center'; +import { Contribution } from './contribution'; + +@injectable() +export class IndexesUpdateProgress extends Contribution { + @inject(NotificationCenter) + private readonly notificationCenter: NotificationCenter; + @inject(ProgressService) + private readonly progressService: ProgressService; + private currentProgress: + | (Progress & Readonly<{ progressId: string }>) + | undefined; + + override onStart(): void { + this.notificationCenter.onIndexWillUpdate((progressId) => + this.getOrCreateProgress(progressId) + ); + this.notificationCenter.onIndexUpdateDidProgress((progress) => { + this.getOrCreateProgress(progress).then((delegate) => + delegate.report(progress) + ); + }); + this.notificationCenter.onIndexDidUpdate((progressId) => { + this.cancelProgress(progressId); + }); + this.notificationCenter.onIndexUpdateDidFail(({ progressId, message }) => { + this.cancelProgress(progressId); + this.messageService.error(message); + }); + } + + private async getOrCreateProgress( + progressOrId: ProgressMessage | string + ): Promise { + const progressId = ProgressMessage.is(progressOrId) + ? progressOrId.progressId + : progressOrId; + if (this.currentProgress?.progressId === progressId) { + return this.currentProgress; + } + if (this.currentProgress) { + this.currentProgress.cancel(); + } + this.currentProgress = undefined; + const progress = await this.progressService.showProgress({ + text: '', + options: { location: 'notification' }, + }); + if (ProgressMessage.is(progressOrId)) { + progress.report(progressOrId); + } + this.currentProgress = { ...progress, progressId }; + return this.currentProgress; + } + + private cancelProgress(progressId: string) { + if (this.currentProgress) { + if (this.currentProgress.progressId !== progressId) { + console.warn( + `Mismatching progress IDs. Expected ${progressId}, got ${this.currentProgress.progressId}. Canceling anyway.` + ); + } + this.currentProgress.cancel(); + this.currentProgress = undefined; + } + } +} diff --git a/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts b/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts index dfedf5d8c..1c1f384ac 100644 --- a/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts @@ -36,7 +36,7 @@ export class OpenRecentSketch extends SketchContribution { protected toDisposeBeforeRegister = new Map(); override onStart(): void { - this.notificationCenter.onRecentSketchesChanged(({ sketches }) => + this.notificationCenter.onRecentSketchesDidChange(({ sketches }) => this.refreshMenu(sketches) ); } 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 dc82199e8..eaeacc520 100644 --- a/arduino-ide-extension/src/browser/library/library-list-widget.ts +++ b/arduino-ide-extension/src/browser/library/library-list-widget.ts @@ -41,8 +41,8 @@ export class LibraryListWidget extends ListWidget { protected override init(): void { super.init(); this.toDispose.pushAll([ - this.notificationCenter.onLibraryInstalled(() => this.refresh(undefined)), - this.notificationCenter.onLibraryUninstalled(() => + this.notificationCenter.onLibraryDidInstall(() => this.refresh(undefined)), + this.notificationCenter.onLibraryDidUninstall(() => this.refresh(undefined) ), ]); diff --git a/arduino-ide-extension/src/browser/notification-center.ts b/arduino-ide-extension/src/browser/notification-center.ts index b6ad3b4b6..b17853d0a 100644 --- a/arduino-ide-extension/src/browser/notification-center.ts +++ b/arduino-ide-extension/src/browser/notification-center.ts @@ -17,6 +17,7 @@ import { LibraryPackage, Config, Sketch, + ProgressMessage, } from '../common/protocol'; import { FrontendApplicationStateService, @@ -33,25 +34,32 @@ export class NotificationCenter @inject(FrontendApplicationStateService) private readonly appStateService: FrontendApplicationStateService; - protected readonly indexUpdatedEmitter = new Emitter(); - protected readonly daemonStartedEmitter = new Emitter(); - protected readonly daemonStoppedEmitter = new Emitter(); - protected readonly configChangedEmitter = new Emitter<{ + protected readonly indexDidUpdateEmitter = new Emitter(); + protected readonly indexWillUpdateEmitter = new Emitter(); + protected readonly indexUpdateDidProgressEmitter = + new Emitter(); + protected readonly indexUpdateDidFailEmitter = new Emitter<{ + progressId: string; + message: string; + }>(); + protected readonly daemonDidStartEmitter = new Emitter(); + protected readonly daemonDidStopEmitter = new Emitter(); + protected readonly configDidChangeEmitter = new Emitter<{ config: Config | undefined; }>(); - protected readonly platformInstalledEmitter = new Emitter<{ + protected readonly platformDidInstallEmitter = new Emitter<{ item: BoardsPackage; }>(); - protected readonly platformUninstalledEmitter = new Emitter<{ + protected readonly platformDidUninstallEmitter = new Emitter<{ item: BoardsPackage; }>(); - protected readonly libraryInstalledEmitter = new Emitter<{ + protected readonly libraryDidInstallEmitter = new Emitter<{ item: LibraryPackage; }>(); - protected readonly libraryUninstalledEmitter = new Emitter<{ + protected readonly libraryDidUninstallEmitter = new Emitter<{ item: LibraryPackage; }>(); - protected readonly attachedBoardsChangedEmitter = + protected readonly attachedBoardsDidChangeEmitter = new Emitter(); protected readonly recentSketchesChangedEmitter = new Emitter<{ sketches: Sketch[]; @@ -60,27 +68,34 @@ export class NotificationCenter 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 + this.indexWillUpdateEmitter, + this.indexUpdateDidProgressEmitter, + this.indexDidUpdateEmitter, + this.indexUpdateDidFailEmitter, + this.daemonDidStartEmitter, + this.daemonDidStopEmitter, + this.configDidChangeEmitter, + this.platformDidInstallEmitter, + this.platformDidUninstallEmitter, + this.libraryDidInstallEmitter, + this.libraryDidUninstallEmitter, + this.attachedBoardsDidChangeEmitter ); - 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; - readonly onRecentSketchesChanged = this.recentSketchesChangedEmitter.event; + readonly onIndexDidUpdate = this.indexDidUpdateEmitter.event; + readonly onIndexWillUpdate = this.indexDidUpdateEmitter.event; + readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event; + readonly onIndexUpdateDidFail = this.indexUpdateDidFailEmitter.event; + readonly onDaemonDidStart = this.daemonDidStartEmitter.event; + readonly onDaemonDidStop = this.daemonDidStopEmitter.event; + readonly onConfigDidChange = this.configDidChangeEmitter.event; + readonly onPlatformDidInstall = this.platformDidInstallEmitter.event; + readonly onPlatformDidUninstall = this.platformDidUninstallEmitter.event; + readonly onLibraryDidInstall = this.libraryDidInstallEmitter.event; + readonly onLibraryDidUninstall = this.libraryDidUninstallEmitter.event; + readonly onAttachedBoardsDidChange = + this.attachedBoardsDidChangeEmitter.event; + readonly onRecentSketchesDidChange = this.recentSketchesChangedEmitter.event; readonly onAppStateDidChange = this.onAppStateDidChangeEmitter.event; @postConstruct() @@ -97,43 +112,61 @@ export class NotificationCenter this.toDispose.dispose(); } - notifyIndexUpdated(): void { - this.indexUpdatedEmitter.fire(); + notifyIndexWillUpdate(progressId: string): void { + this.indexWillUpdateEmitter.fire(progressId); + } + + notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void { + this.indexUpdateDidProgressEmitter.fire(progressMessage); + } + + notifyIndexDidUpdate(progressId: string): void { + this.indexDidUpdateEmitter.fire(progressId); + } + + notifyIndexUpdateDidFail({ + progressId, + message, + }: { + progressId: string; + message: string; + }): void { + this.indexUpdateDidFailEmitter.fire({ progressId, message }); } - notifyDaemonStarted(port: string): void { - this.daemonStartedEmitter.fire(port); + notifyDaemonDidStart(port: string): void { + this.daemonDidStartEmitter.fire(port); } - notifyDaemonStopped(): void { - this.daemonStoppedEmitter.fire(); + notifyDaemonDidStop(): void { + this.daemonDidStopEmitter.fire(); } - notifyConfigChanged(event: { config: Config | undefined }): void { - this.configChangedEmitter.fire(event); + notifyConfigDidChange(event: { config: Config | undefined }): void { + this.configDidChangeEmitter.fire(event); } - notifyPlatformInstalled(event: { item: BoardsPackage }): void { - this.platformInstalledEmitter.fire(event); + notifyPlatformDidInstall(event: { item: BoardsPackage }): void { + this.platformDidInstallEmitter.fire(event); } - notifyPlatformUninstalled(event: { item: BoardsPackage }): void { - this.platformUninstalledEmitter.fire(event); + notifyPlatformDidUninstall(event: { item: BoardsPackage }): void { + this.platformDidUninstallEmitter.fire(event); } - notifyLibraryInstalled(event: { item: LibraryPackage }): void { - this.libraryInstalledEmitter.fire(event); + notifyLibraryDidInstall(event: { item: LibraryPackage }): void { + this.libraryDidInstallEmitter.fire(event); } - notifyLibraryUninstalled(event: { item: LibraryPackage }): void { - this.libraryUninstalledEmitter.fire(event); + notifyLibraryDidUninstall(event: { item: LibraryPackage }): void { + this.libraryDidUninstallEmitter.fire(event); } - notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { - this.attachedBoardsChangedEmitter.fire(event); + notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void { + this.attachedBoardsDidChangeEmitter.fire(event); } - notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void { + notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void { this.recentSketchesChangedEmitter.fire(event); } } diff --git a/arduino-ide-extension/src/browser/response-service-impl.ts b/arduino-ide-extension/src/browser/response-service-impl.ts index c50506c86..29a016096 100644 --- a/arduino-ide-extension/src/browser/response-service-impl.ts +++ b/arduino-ide-extension/src/browser/response-service-impl.ts @@ -7,11 +7,11 @@ import { import { OutputMessage, ProgressMessage, - ResponseServiceArduino, + ResponseServiceClient, } from '../common/protocol/response-service'; @injectable() -export class ResponseServiceImpl implements ResponseServiceArduino { +export class ResponseServiceImpl implements ResponseServiceClient { @inject(OutputChannelManager) private readonly outputChannelManager: OutputChannelManager; @@ -19,7 +19,7 @@ export class ResponseServiceImpl implements ResponseServiceArduino { readonly onProgressDidChange = this.progressDidChangeEmitter.event; - clearArduinoChannel(): void { + clearOutput(): void { this.outputChannelManager.getChannel('Arduino').clear(); } 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 ae997183f..343a13985 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 @@ -30,10 +30,10 @@ export class FrontendConnectionStatusService extends TheiaFrontendConnectionStat try { this.connectedPort = await this.daemon.tryGetPort(); } catch {} - this.notificationCenter.onDaemonStarted( + this.notificationCenter.onDaemonDidStart( (port) => (this.connectedPort = port) ); - this.notificationCenter.onDaemonStopped( + this.notificationCenter.onDaemonDidStop( () => (this.connectedPort = undefined) ); this.wsConnectionProvider.onIncomingMessageActivity(() => { @@ -58,10 +58,10 @@ export class ApplicationConnectionStatusContribution extends TheiaApplicationCon try { this.connectedPort = await this.daemon.tryGetPort(); } catch {} - this.notificationCenter.onDaemonStarted( + this.notificationCenter.onDaemonDidStart( (port) => (this.connectedPort = port) ); - this.notificationCenter.onDaemonStopped( + this.notificationCenter.onDaemonDidStop( () => (this.connectedPort = undefined) ); } diff --git a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx index 0fad3ac61..194e0405c 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx @@ -11,7 +11,7 @@ import { SearchBar } from './search-bar'; import { ListWidget } from './list-widget'; import { ComponentList } from './component-list'; import { ListItemRenderer } from './list-item-renderer'; -import { ResponseServiceArduino } from '../../../common/protocol'; +import { ResponseServiceClient } from '../../../common/protocol'; import { nls } from '@theia/core/lib/common'; export class FilterableListContainer< @@ -162,7 +162,7 @@ export namespace FilterableListContainer { readonly resolveFocus: (element: HTMLElement | undefined) => void; readonly filterTextChangeEvent: Event; readonly messageService: MessageService; - readonly responseService: ResponseServiceArduino; + readonly responseService: ResponseServiceClient; readonly install: ({ item, progressId, 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 f28db5d5b..3e562c140 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 @@ -1,5 +1,9 @@ import * as React from '@theia/core/shared/react'; -import { injectable, postConstruct, inject } from '@theia/core/shared/inversify'; +import { + injectable, + postConstruct, + inject, +} from '@theia/core/shared/inversify'; import { Widget } from '@theia/core/shared/@phosphor/widgets'; import { Message } from '@theia/core/shared/@phosphor/messaging'; import { Deferred } from '@theia/core/lib/common/promise-util'; @@ -12,7 +16,7 @@ import { Installable, Searchable, ArduinoComponent, - ResponseServiceArduino, + ResponseServiceClient, } from '../../../common/protocol'; import { FilterableListContainer } from './filterable-list-container'; import { ListItemRenderer } from './list-item-renderer'; @@ -21,15 +25,15 @@ import { NotificationCenter } from '../../notification-center'; @injectable() export abstract class ListWidget< T extends ArduinoComponent - > extends ReactWidget { +> extends ReactWidget { @inject(MessageService) protected readonly messageService: MessageService; @inject(CommandService) protected readonly commandService: CommandService; - @inject(ResponseServiceArduino) - protected readonly responseService: ResponseServiceArduino; + @inject(ResponseServiceClient) + protected readonly responseService: ResponseServiceClient; @inject(NotificationCenter) protected readonly notificationCenter: NotificationCenter; @@ -67,9 +71,9 @@ export abstract class ListWidget< @postConstruct() protected init(): void { this.toDispose.pushAll([ - this.notificationCenter.onIndexUpdated(() => this.refresh(undefined)), - this.notificationCenter.onDaemonStarted(() => this.refresh(undefined)), - this.notificationCenter.onDaemonStopped(() => this.refresh(undefined)), + this.notificationCenter.onIndexDidUpdate(() => this.refresh(undefined)), + this.notificationCenter.onDaemonDidStart(() => this.refresh(undefined)), + this.notificationCenter.onDaemonDidStop(() => 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 696629923..b59d4c617 100644 --- a/arduino-ide-extension/src/common/protocol/arduino-daemon.ts +++ b/arduino-ide-extension/src/common/protocol/arduino-daemon.ts @@ -12,4 +12,7 @@ export interface ArduinoDaemon { * Otherwise resolves to the CLI daemon port. */ tryGetPort(): Promise; + start(): Promise; + stop(): Promise; + restart(): Promise; } diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index a783eae28..debf0b9fb 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -1,10 +1,13 @@ -import { ApplicationError } from '@theia/core'; -import { Location } from '@theia/core/shared/vscode-languageserver-protocol'; -import { BoardUserField } from '.'; -import { Board, Port } from '../../common/protocol/boards-service'; -import { ErrorInfo as CliErrorInfo } from '../../node/cli-error-parser'; -import { Programmer } from './boards-service'; -import { Sketch } from './sketches-service'; +import { ApplicationError } from '@theia/core/lib/common/application-error'; +import type { Location } from '@theia/core/shared/vscode-languageserver-protocol'; +import type { + Board, + BoardUserField, + Port, +} from '../../common/protocol/boards-service'; +import type { ErrorInfo as CliErrorInfo } from '../../node/cli-error-parser'; +import type { Programmer } from './boards-service'; +import type { Sketch } from './sketches-service'; export const CompilerWarningLiterals = [ 'None', diff --git a/arduino-ide-extension/src/common/protocol/installable.ts b/arduino-ide-extension/src/common/protocol/installable.ts index 206039408..e0e2e6346 100644 --- a/arduino-ide-extension/src/common/protocol/installable.ts +++ b/arduino-ide-extension/src/common/protocol/installable.ts @@ -1,13 +1,13 @@ import * as semver from 'semver'; -import { Progress } from '@theia/core/lib/common/message-service-protocol'; +import type { Progress } from '@theia/core/lib/common/message-service-protocol'; import { CancellationToken, CancellationTokenSource, } from '@theia/core/lib/common/cancellation'; import { naturalCompare } from './../utils'; -import { ArduinoComponent } from './arduino-component'; -import { MessageService } from '@theia/core'; -import { ResponseServiceArduino } from './response-service'; +import type { ArduinoComponent } from './arduino-component'; +import type { MessageService } from '@theia/core/lib/common/message-service'; +import type { ResponseServiceClient } from './response-service'; export interface Installable { /** @@ -44,7 +44,7 @@ export namespace Installable { >(options: { installable: Installable; messageService: MessageService; - responseService: ResponseServiceArduino; + responseService: ResponseServiceClient; item: T; version: Installable.Version; }): Promise { @@ -66,7 +66,7 @@ export namespace Installable { >(options: { installable: Installable; messageService: MessageService; - responseService: ResponseServiceArduino; + responseService: ResponseServiceClient; item: T; }): Promise { const { item } = options; @@ -86,7 +86,7 @@ export namespace Installable { export async function doWithProgress(options: { run: ({ progressId }: { progressId: string }) => Promise; messageService: MessageService; - responseService: ResponseServiceArduino; + responseService: ResponseServiceClient; progressText: string; }): Promise { return withProgress( @@ -103,7 +103,7 @@ export namespace Installable { } ); try { - options.responseService.clearArduinoChannel(); + options.responseService.clearOutput(); await options.run({ progressId }); } finally { toDispose.dispose(); diff --git a/arduino-ide-extension/src/common/protocol/notification-service.ts b/arduino-ide-extension/src/common/protocol/notification-service.ts index 3e33f727e..e1b192ece 100644 --- a/arduino-ide-extension/src/common/protocol/notification-service.ts +++ b/arduino-ide-extension/src/common/protocol/notification-service.ts @@ -1,23 +1,33 @@ -import { LibraryPackage } from './library-service'; -import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; -import { - Sketch, - Config, - BoardsPackage, +import type { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; +import type { AttachedBoardsChangeEvent, + BoardsPackage, + Config, + ProgressMessage, + Sketch, } from '../protocol'; +import type { LibraryPackage } from './library-service'; export interface NotificationServiceClient { - notifyIndexUpdated(): void; - notifyDaemonStarted(port: string): 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; - notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void; + notifyIndexWillUpdate(progressId: string): void; + notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void; + notifyIndexDidUpdate(progressId: string): void; + notifyIndexUpdateDidFail({ + progressId, + message, + }: { + progressId: string; + message: string; + }): void; + notifyDaemonDidStart(port: string): void; + notifyDaemonDidStop(): void; + notifyConfigDidChange(event: { config: Config | undefined }): void; + notifyPlatformDidInstall(event: { item: BoardsPackage }): void; + notifyPlatformDidUninstall(event: { item: BoardsPackage }): void; + notifyLibraryDidInstall(event: { item: LibraryPackage }): void; + notifyLibraryDidUninstall(event: { item: LibraryPackage }): void; + notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void; + notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void; } export const NotificationServicePath = '/services/notification-service'; diff --git a/arduino-ide-extension/src/common/protocol/response-service.ts b/arduino-ide-extension/src/common/protocol/response-service.ts index 9c2e4e248..d54be98f7 100644 --- a/arduino-ide-extension/src/common/protocol/response-service.ts +++ b/arduino-ide-extension/src/common/protocol/response-service.ts @@ -1,4 +1,4 @@ -import { Event } from '@theia/core/lib/common/event'; +import type { Event } from '@theia/core/lib/common/event'; export interface OutputMessage { readonly chunk: string; @@ -18,6 +18,18 @@ export interface ProgressMessage { readonly work?: ProgressMessage.Work; } export namespace ProgressMessage { + export function is(arg: unknown): arg is ProgressMessage { + if (typeof arg === 'object') { + const object = arg as Record; + return ( + 'progressId' in object && + typeof object.progressId === 'string' && + 'message' in object && + typeof object.message === 'string' + ); + } + return false; + } export interface Work { readonly done: number; readonly total: number; @@ -31,8 +43,8 @@ export interface ResponseService { reportProgress(message: ProgressMessage): void; } -export const ResponseServiceArduino = Symbol('ResponseServiceArduino'); -export interface ResponseServiceArduino extends ResponseService { +export const ResponseServiceClient = Symbol('ResponseServiceClient'); +export interface ResponseServiceClient extends ResponseService { onProgressDidChange: Event; - clearArduinoChannel: () => void; + clearOutput: () => void; } diff --git a/arduino-ide-extension/src/node/arduino-daemon-impl.ts b/arduino-ide-extension/src/node/arduino-daemon-impl.ts index 928861680..f859985e4 100644 --- a/arduino-ide-extension/src/node/arduino-daemon-impl.ts +++ b/arduino-ide-extension/src/node/arduino-daemon-impl.ts @@ -4,7 +4,7 @@ import { inject, injectable, named } from '@theia/core/shared/inversify'; import { spawn, ChildProcess } from 'child_process'; import { FileUri } from '@theia/core/lib/node/file-uri'; import { ILogger } from '@theia/core/lib/common/logger'; -import { Deferred } from '@theia/core/lib/common/promise-util'; +import { Deferred, retry } from '@theia/core/lib/common/promise-util'; import { Disposable, DisposableCollection, @@ -23,26 +23,26 @@ export class ArduinoDaemonImpl { @inject(ILogger) @named('daemon') - protected readonly logger: ILogger; + private readonly logger: ILogger; @inject(EnvVariablesServer) - protected readonly envVariablesServer: EnvVariablesServer; + private readonly envVariablesServer: EnvVariablesServer; @inject(NotificationServiceServer) - protected readonly notificationService: NotificationServiceServer; + private readonly notificationService: NotificationServiceServer; - protected readonly toDispose = new DisposableCollection(); - protected readonly onDaemonStartedEmitter = new Emitter(); - protected readonly onDaemonStoppedEmitter = new Emitter(); + private readonly toDispose = new DisposableCollection(); + private readonly onDaemonStartedEmitter = new Emitter(); + private readonly onDaemonStoppedEmitter = new Emitter(); - protected _running = false; - protected _port = new Deferred(); - protected _execPath: string | undefined; + private _running = false; + private _port = new Deferred(); + private _execPath: string | undefined; // Backend application lifecycle. onStart(): void { - this.startDaemon(); // no await + this.start(); // no await } // Daemon API @@ -58,7 +58,7 @@ export class ArduinoDaemonImpl return undefined; } - async startDaemon(): Promise { + async start(): Promise { try { this.toDispose.dispose(); // This will `kill` the previously started daemon process, if any. const cliPath = await this.getExecPath(); @@ -86,24 +86,29 @@ export class ArduinoDaemonImpl ]); this.fireDaemonStarted(port); this.onData('Daemon is running.'); + return port; } catch (err) { - this.onData('Failed to start the daemon.'); - this.onError(err); - let i = 5; // TODO: make this better - while (i) { - this.onData(`Restarting daemon in ${i} seconds...`); - await new Promise((resolve) => setTimeout(resolve, 1000)); - i--; - } - this.onData('Restarting daemon now...'); - return this.startDaemon(); + return retry( + () => { + this.onError(err); + return this.start(); + }, + 1_000, + 5 + ); } } - async stopDaemon(): Promise { + async stop(): Promise { this.toDispose.dispose(); } + async restart(): Promise { + return this.start(); + } + + // Backend only daemon API + get onDaemonStarted(): Event { return this.onDaemonStartedEmitter.event; } @@ -275,14 +280,14 @@ export class ArduinoDaemonImpl return ready.promise; } - protected fireDaemonStarted(port: string): void { + private fireDaemonStarted(port: string): void { this._running = true; this._port.resolve(port); this.onDaemonStartedEmitter.fire(port); - this.notificationService.notifyDaemonStarted(port); + this.notificationService.notifyDaemonDidStart(port); } - protected fireDaemonStopped(): void { + private fireDaemonStopped(): void { if (!this._running) { return; } @@ -290,14 +295,15 @@ export class ArduinoDaemonImpl this._port.reject(); // Reject all pending. this._port = new Deferred(); this.onDaemonStoppedEmitter.fire(); - this.notificationService.notifyDaemonStopped(); + this.notificationService.notifyDaemonDidStop(); } protected onData(message: string): void { this.logger.info(message); } - protected onError(error: any): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private onError(error: any): void { this.logger.error(error); } } 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 97d52c863..346668d0c 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -18,7 +18,7 @@ import { BoardsService, BoardsServicePath, } from '../common/protocol/boards-service'; -import { LibraryServiceImpl } from './library-service-server-impl'; +import { LibraryServiceImpl } from './library-service-impl'; import { BoardsServiceImpl } from './boards-service-impl'; import { CoreServiceImpl } from './core-service-impl'; import { CoreService, CoreServicePath } from '../common/protocol/core-service'; @@ -245,7 +245,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { const webSocketProvider = container.get(WebSocketProvider); - const { board, port, coreClientProvider, monitorID } = options; + const { board, port, monitorID } = options; return new MonitorService( logger, @@ -253,8 +253,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { webSocketProvider, board, port, - monitorID, - coreClientProvider + monitorID ); } ); diff --git a/arduino-ide-extension/src/node/board-discovery.ts b/arduino-ide-extension/src/node/board-discovery.ts index dcaa43d92..f2d2dcb41 100644 --- a/arduino-ide-extension/src/node/board-discovery.ts +++ b/arduino-ide-extension/src/node/board-discovery.ts @@ -55,7 +55,7 @@ export class BoardDiscovery extends CoreClientAware { @postConstruct() protected async init(): Promise { - this.coreClient().then((client) => this.startBoardListWatch(client)); + this.coreClient.then((client) => this.startBoardListWatch(client)); } stopBoardListWatch(coreClient: CoreClientProvider.Client): Promise { @@ -181,7 +181,7 @@ export class BoardDiscovery extends CoreClientAware { }; this._state = newState; - this.notificationService.notifyAttachedBoardsChanged(event); + this.notificationService.notifyAttachedBoardsDidChange(event); } }); this.boardWatchDuplex.write(req); diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 089da2f74..98cdcb7fb 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -40,7 +40,7 @@ import { SupportedUserFieldsRequest, SupportedUserFieldsResponse, } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; -import { InstallWithProgress } from './grpc-installable'; +import { ExecuteWithProgress } from './grpc-progressible'; @injectable() export class BoardsServiceImpl @@ -78,8 +78,7 @@ export class BoardsServiceImpl async getBoardDetails(options: { fqbn: string; }): Promise { - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const { fqbn } = options; const detailsReq = new BoardDetailsRequest(); @@ -218,8 +217,7 @@ export class BoardsServiceImpl }: { query?: string; }): Promise { - await this.coreClientProvider.initialized; - const { instance, client } = await this.coreClient(); + const { instance, client } = await this.coreClient; const req = new BoardSearchRequest(); req.setSearchArgs(query || ''); req.setInstance(instance); @@ -252,8 +250,7 @@ export class BoardsServiceImpl fqbn: string; protocol: string; }): Promise { - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const supportedUserFieldsReq = new SupportedUserFieldsRequest(); @@ -279,8 +276,7 @@ export class BoardsServiceImpl } async search(options: { query?: string }): Promise { - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const installedPlatformsReq = new PlatformListRequest(); @@ -404,8 +400,7 @@ export class BoardsServiceImpl const version = !!options.version ? options.version : item.availableVersions[0]; - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const [platform, architecture] = item.id.split(':'); @@ -424,7 +419,7 @@ export class BoardsServiceImpl const resp = client.platformInstall(req); resp.on( 'data', - InstallWithProgress.createDataCallback({ + ExecuteWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService, }) @@ -448,7 +443,7 @@ export class BoardsServiceImpl const items = await this.search({}); const updated = items.find((other) => BoardsPackage.equals(other, item)) || item; - this.notificationService.notifyPlatformInstalled({ item: updated }); + this.notificationService.notifyPlatformDidInstall({ item: updated }); console.info('<<< Boards package installation done.', item); } @@ -457,8 +452,7 @@ export class BoardsServiceImpl progressId?: string; }): Promise { const { item, progressId } = options; - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const [platform, architecture] = item.id.split(':'); @@ -476,7 +470,7 @@ export class BoardsServiceImpl const resp = client.platformUninstall(req); resp.on( 'data', - InstallWithProgress.createDataCallback({ + ExecuteWithProgress.createDataCallback({ progressId, responseService: this.responseService, }) @@ -490,7 +484,7 @@ export class BoardsServiceImpl }); // Here, unlike at `install` we send out the argument `item`. Otherwise, we would not know about the board FQBN. - this.notificationService.notifyPlatformUninstalled({ item }); + this.notificationService.notifyPlatformDidUninstall({ 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 420d4185f..27c560856 100644 --- a/arduino-ide-extension/src/node/config-service-impl.ts +++ b/arduino-ide-extension/src/node/config-service-impl.ts @@ -199,11 +199,11 @@ export class ConfigServiceImpl protected fireConfigChanged(config: Config): void { this.configChangeEmitter.fire(config); - this.notificationService.notifyConfigChanged({ config }); + this.notificationService.notifyConfigDidChange({ config }); } protected fireInvalidConfig(): void { - this.notificationService.notifyConfigChanged({ config: undefined }); + this.notificationService.notifyConfigDidChange({ config: undefined }); } protected async updateDaemon( diff --git a/arduino-ide-extension/src/node/core-client-provider.ts b/arduino-ide-extension/src/node/core-client-provider.ts index 271248706..404b76421 100644 --- a/arduino-ide-extension/src/node/core-client-provider.ts +++ b/arduino-ide-extension/src/node/core-client-provider.ts @@ -1,18 +1,18 @@ +import { join } from 'path'; import * as grpc from '@grpc/grpc-js'; import { inject, injectable, postConstruct, } from '@theia/core/shared/inversify'; -import { Event, Emitter } from '@theia/core/lib/common/event'; -import { GrpcClientProvider } from './grpc-client-provider'; +import { Emitter } from '@theia/core/lib/common/event'; import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb'; import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; import { CreateRequest, - CreateResponse, InitRequest, InitResponse, + UpdateCoreLibrariesIndexResponse, UpdateIndexRequest, UpdateIndexResponse, UpdateLibrariesIndexRequest, @@ -25,263 +25,363 @@ import { Status as RpcStatus, Status, } from './cli-protocol/google/rpc/status_pb'; +import { ConfigServiceImpl } from './config-service-impl'; +import { ArduinoDaemonImpl } from './arduino-daemon-impl'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; +import { + IndexesUpdateProgressHandler, + ExecuteWithProgress, +} from './grpc-progressible'; +import type { DefaultCliConfig } from './cli-config'; +import { ServiceError } from './service-error'; @injectable() -export class CoreClientProvider extends GrpcClientProvider { +export class CoreClientProvider { + @inject(ArduinoDaemonImpl) + private readonly daemon: ArduinoDaemonImpl; + @inject(ConfigServiceImpl) + private readonly configService: ConfigServiceImpl; @inject(NotificationServiceServer) - protected readonly notificationService: NotificationServiceServer; - - protected readonly onClientReadyEmitter = new Emitter(); + private readonly notificationService: NotificationServiceServer; - protected _created = new Deferred(); - protected _initialized = new Deferred(); + private ready = new Deferred(); + private pending: Deferred | undefined; + private _client: CoreClientProvider.Client | undefined; + private readonly toDisposeBeforeCreate = new DisposableCollection(); + private readonly toDisposeAfterDidCreate = new DisposableCollection(); + private readonly onClientReadyEmitter = + new Emitter(); + private readonly onClientReady = this.onClientReadyEmitter.event; - get created(): Promise { - return this._created.promise; + @postConstruct() + protected init(): void { + this.daemon.tryGetPort().then((port) => { + if (port) { + this.create(port); + } + }); + this.daemon.onDaemonStarted((port) => this.create(port)); + this.daemon.onDaemonStopped(() => this.closeClient()); + this.configService.onConfigChange(() => this.refreshIndexes()); } - get initialized(): Promise { - return this._initialized.promise; + get tryGetClient(): CoreClientProvider.Client | undefined { + return this._client; } - get onClientReady(): Event { - return this.onClientReadyEmitter.event; + get client(): Promise { + const client = this.tryGetClient; + if (client) { + return Promise.resolve(client); + } + if (!this.pending) { + this.pending = new Deferred(); + this.toDisposeAfterDidCreate.pushAll([ + Disposable.create(() => (this.pending = undefined)), + this.onClientReady((client) => { + this.pending?.resolve(client); + this.toDisposeAfterDidCreate.dispose(); + }), + ]); + } + return this.pending.promise; } - close(client: CoreClientProvider.Client): void { - client.client.close(); - this._created.reject(); - this._initialized.reject(); - this._created = new Deferred(); - this._initialized = new Deferred(); + /** + * Encapsulates both the gRPC core client creation (`CreateRequest`) and initialization (`InitRequest`). + */ + private async create(port: string): Promise { + this.closeClient(); + const address = this.address(port); + const client = await this.createClient(address); + this.toDisposeBeforeCreate.pushAll([ + Disposable.create(() => client.client.close()), + Disposable.create(() => { + this.ready.reject( + new Error( + `Disposed. Creating a new gRPC core client on address ${address}.` + ) + ); + this.ready = new Deferred(); + }), + ]); + await this.initInstanceWithFallback(client); + setTimeout(async () => this.refreshIndexes(), 10_000); // Update the indexes asynchronously + return this.useClient(client); } - protected override async reconcileClient(port: string): Promise { - if (port && port === this._port) { - // No need to create a new gRPC client, but we have to update the indexes. - if (this._client && !(this._client instanceof Error)) { - await this.updateIndexes(this._client); - this.onClientReadyEmitter.fire(); + /** + * By default, calling this method is equivalent to the `initInstance(Client)` call. + * When the IDE2 starts and one of the followings is missing, + * the IDE2 must run the index update before the core client initialization: + * + * - primary package index (`#directories.data/package_index.json`), + * - library index (`#directories.data/library_index.json`), + * - built-in tools (`builtin:serial-discovery` or `builtin:mdns-discovery`) + * + * This method detects such errors and runs an index update before initializing the client. + * The index update will fail if the 3rd URLs list contains an invalid URL, + * and the IDE2 will be [non-functional](https://github.com/arduino/arduino-ide/issues/1084). Since the CLI [cannot update only the primary package index]((https://github.com/arduino/arduino-cli/issues/1788)), IDE2 does its dirty solution. + */ + private async initInstanceWithFallback( + client: CoreClientProvider.Client + ): Promise { + try { + await this.initInstance(client); + } catch (err) { + if (err instanceof IndexUpdateRequiredBeforeInitError) { + console.error( + 'The primary packages indexes are missing. Running indexes update before initializing the core gRPC client', + err.message + ); + await this.updateIndexes(client); // TODO: this should run without the 3rd party URLs + await this.initInstance(client); + console.info( + `Downloaded the primary packages indexes, and successfully initialized the core gRPC client.` + ); + } else { + console.error( + 'Error occurred while initializing the core gRPC client provider', + err + ); + throw err; } - } else { - await super.reconcileClient(port); - this.onClientReadyEmitter.fire(); } } - @postConstruct() - protected override init(): void { - this.daemon.getPort().then(async (port) => { - // First create the client and the instance synchronously - // and notify client is ready. - // TODO: Creation failure should probably be handled here - await this.reconcileClient(port); // create instance - this._created.resolve(); - - // Normal startup workflow: - // 1. create instance, - // 2. init instance, - // 3. update indexes asynchronously. - - // First startup workflow: - // 1. create instance, - // 2. update indexes and wait, - // 3. init instance. - if (this._client && !(this._client instanceof Error)) { - try { - await this.initInstance(this._client); // init instance - this._initialized.resolve(); - this.updateIndex(this._client); // Update the indexes asynchronously - } catch (error: unknown) { - console.error( - 'Error occurred while initializing the core gRPC client provider', - error - ); - if (error instanceof IndexUpdateRequiredBeforeInitError) { - // If it's a first start, IDE2 must run index update before the init request. - await this.updateIndexes(this._client); - await this.initInstance(this._client); - this._initialized.resolve(); - } else { - throw error; - } - } - } - }); + private useClient( + client: CoreClientProvider.Client + ): CoreClientProvider.Client { + this._client = client; + this.onClientReadyEmitter.fire(this._client); + return this._client; + } - this.daemon.onDaemonStopped(() => { - if (this._client && !(this._client instanceof Error)) { - this.close(this._client); - } - this._client = undefined; - this._port = undefined; - }); + private closeClient(): void { + return this.toDisposeBeforeCreate.dispose(); } - protected async createClient( - port: string | number + private async createClient( + address: string ): Promise { // https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage const ArduinoCoreServiceClient = grpc.makeClientConstructor( // @ts-expect-error: ignore commandsGrpcPb['cc.arduino.cli.commands.v1.ArduinoCoreService'], 'ArduinoCoreServiceService' + // eslint-disable-next-line @typescript-eslint/no-explicit-any ) as any; const client = new ArduinoCoreServiceClient( - `localhost:${port}`, + address, grpc.credentials.createInsecure(), this.channelOptions ) as ArduinoCoreServiceClient; - const createRes = await new Promise((resolve, reject) => { - client.create(new CreateRequest(), (err, res: CreateResponse) => { + const instance = await new Promise((resolve, reject) => { + client.create(new CreateRequest(), (err, resp) => { if (err) { reject(err); return; } - resolve(res); + const instance = resp.getInstance(); + if (!instance) { + reject( + new Error( + '`CreateResponse` was OK, but the retrieved `instance` was `undefined`.' + ) + ); + return; + } + resolve(instance); }); }); - const instance = createRes.getInstance(); - if (!instance) { - throw new Error( - 'Could not retrieve instance from the initialize response.' - ); - } - return { instance, client }; } - protected async initInstance({ + private async initInstance({ client, instance, }: CoreClientProvider.Client): Promise { - const initReq = new InitRequest(); - initReq.setInstance(instance); return new Promise((resolve, reject) => { - const stream = client.init(initReq); const errors: RpcStatus[] = []; - stream.on('data', (res: InitResponse) => { - const progress = res.getInitProgress(); - if (progress) { - const downloadProgress = progress.getDownloadProgress(); - if (downloadProgress && downloadProgress.getCompleted()) { - const file = downloadProgress.getFile(); - console.log(`Downloaded ${file}`); + client + .init(new InitRequest().setInstance(instance)) + .on('data', (resp: InitResponse) => { + // XXX: The CLI never sends `initProgress`, it's always `error` or nothing. Is this a CLI bug? + // According to the gRPC API, the CLI should send either a `TaskProgress` or a `DownloadProgress`, but it does not. + const error = resp.getError(); + if (error) { + const { code, message } = Status.toObject(false, error); + console.error( + `Detected an error response during the gRPC core client initialization: code: ${code}, message: ${message}` + ); + errors.push(error); } - const taskProgress = progress.getTaskProgress(); - if (taskProgress && taskProgress.getCompleted()) { - const name = taskProgress.getName(); - console.log(`Completed ${name}`); + }) + .on('error', reject) + .on('end', () => { + const error = this.evaluateErrorStatus(errors); + if (error) { + reject(error); + return; } - } - - const error = res.getError(); - if (error) { - const { code, message } = Status.toObject(false, error); - console.error( - `Detected an error response during the gRPC core client initialization: code: ${code}, message: ${message}` - ); - errors.push(error); - } - }); - stream.on('error', reject); - stream.on('end', () => { - const error = this.evaluateErrorStatus(errors); - if (error) { - reject(error); - return; - } - resolve(); - }); + resolve(); + }); }); } private evaluateErrorStatus(status: RpcStatus[]): Error | undefined { - const error = isIndexUpdateRequiredBeforeInit(status); // put future error matching here - return error; + const { cliConfiguration } = this.configService; + if (!cliConfiguration) { + // If the CLI config is not available, do not even try to guess what went wrong. + return new Error(`Could not read the CLI configuration file.`); + } + return isIndexUpdateRequiredBeforeInit(status, cliConfiguration); // put future error matching here } - protected async updateIndexes( - client: CoreClientProvider.Client - ): Promise { + /** + * Updates all indexes and runs an init to [reload the indexes](https://github.com/arduino/arduino-cli/pull/1274#issue-866154638). + */ + private async refreshIndexes(): Promise { + const client = this._client; + if (client) { + const progressHandler = this.createProgressHandler(); + try { + await this.updateIndexes(client, progressHandler); + await this.initInstance(client); + // notify clients about the index update only after the client has been "re-initialized" and the new content is available. + progressHandler.reportEnd(); + } catch (err) { + console.error('Failed to update indexes', err); + progressHandler.reportError( + ServiceError.is(err) ? err.details : String(err) + ); + } + } + } + + private async updateIndexes( + client: CoreClientProvider.Client, + progressHandler?: IndexesUpdateProgressHandler + ): Promise { await Promise.all([ - retry(() => this.updateIndex(client), 50, 3), - retry(() => this.updateLibraryIndex(client), 50, 3), + this.updateIndex(client, progressHandler), + this.updateLibraryIndex(client, progressHandler), ]); - this.notificationService.notifyIndexUpdated(); - return client; } - protected async updateLibraryIndex({ - client, - instance, - }: CoreClientProvider.Client): Promise { - const req = new UpdateLibrariesIndexRequest(); - req.setInstance(instance); - const resp = client.updateLibrariesIndex(req); - let file: string | undefined; - resp.on('data', (data: UpdateLibrariesIndexResponse) => { - const progress = data.getDownloadProgress(); - if (progress) { - if (!file && progress.getFile()) { - file = `${progress.getFile()}`; - } - if (progress.getCompleted()) { - if (file) { - if (/\s/.test(file)) { - console.log(`${file} completed.`); - } else { - console.log(`Download of '${file}' completed.`); - } - } else { - console.log('The library index has been successfully updated.'); - } - file = undefined; - } - } - }); - await new Promise((resolve, reject) => { - resp.on('error', (error) => { - reject(error); - }); - resp.on('end', resolve); - }); + private async updateIndex( + client: CoreClientProvider.Client, + progressHandler?: IndexesUpdateProgressHandler + ): Promise { + return this.doUpdateIndex( + () => + client.client.updateIndex( + new UpdateIndexRequest().setInstance(client.instance) + ), + progressHandler, + 'platform-index' + ); } - protected async updateIndex({ - client, - instance, - }: CoreClientProvider.Client): Promise { - const updateReq = new UpdateIndexRequest(); - updateReq.setInstance(instance); - const updateResp = client.updateIndex(updateReq); - let file: string | undefined; - updateResp.on('data', (o: UpdateIndexResponse) => { - const progress = o.getDownloadProgress(); - if (progress) { - if (!file && progress.getFile()) { - file = `${progress.getFile()}`; - } - if (progress.getCompleted()) { - if (file) { - if (/\s/.test(file)) { - console.log(`${file} completed.`); - } else { - console.log(`Download of '${file}' completed.`); - } - } else { - console.log('The index has been successfully updated.'); - } - file = undefined; - } - } - }); - await new Promise((resolve, reject) => { - updateResp.on('error', reject); - updateResp.on('end', resolve); - }); + private async updateLibraryIndex( + client: CoreClientProvider.Client, + progressHandler?: IndexesUpdateProgressHandler + ): Promise { + return this.doUpdateIndex( + () => + client.client.updateLibrariesIndex( + new UpdateLibrariesIndexRequest().setInstance(client.instance) + ), + progressHandler, + 'library-index' + ); + } + + private async doUpdateIndex< + R extends + | UpdateIndexResponse + | UpdateLibrariesIndexResponse + | UpdateCoreLibrariesIndexResponse // not used by IDE2 + >( + responseProvider: () => grpc.ClientReadableStream, + progressHandler?: IndexesUpdateProgressHandler, + task?: string + ): Promise { + const progressId = progressHandler?.progressId; + return retry( + () => + new Promise((resolve, reject) => { + responseProvider() + .on( + 'data', + ExecuteWithProgress.createDataCallback({ + responseService: { + appendToOutput: ({ chunk: message }) => { + console.log( + `core-client-provider${task ? ` [${task}]` : ''}`, + message + ); + progressHandler?.reportProgress(message); + }, + }, + progressId, + }) + ) + .on('error', reject) + .on('end', resolve); + }), + 50, + 3 + ); + } + + private createProgressHandler(): IndexesUpdateProgressHandler { + const additionalUrlsCount = + this.configService.cliConfiguration?.board_manager?.additional_urls + ?.length ?? 0; + return new IndexesUpdateProgressHandler( + additionalUrlsCount, + (progressMessage) => + this.notificationService.notifyIndexUpdateDidProgress(progressMessage), + ({ progressId, message }) => + this.notificationService.notifyIndexUpdateDidFail({ + progressId, + message, + }), + (progressId) => + this.notificationService.notifyIndexWillUpdate(progressId), + (progressId) => this.notificationService.notifyIndexDidUpdate(progressId) + ); + } + + private address(port: string): string { + return `localhost:${port}`; + } + + private get channelOptions(): Record { + return { + 'grpc.max_send_message_length': 512 * 1024 * 1024, + 'grpc.max_receive_message_length': 512 * 1024 * 1024, + 'grpc.primary_user_agent': `arduino-ide/${this.version}`, + }; + } + + private _version: string | undefined; + private get version(): string { + if (this._version) { + return this._version; + } + const json = require('../../package.json'); + if ('version' in json) { + this._version = json.version; + } + if (!this._version) { + this._version = '0.0.0'; + } + return this._version; } } export namespace CoreClientProvider { @@ -291,22 +391,18 @@ export namespace CoreClientProvider { } } +/** + * Sugar for making the gRPC core client available for the concrete service classes. + */ @injectable() export abstract class CoreClientAware { @inject(CoreClientProvider) - protected readonly coreClientProvider: CoreClientProvider; - - protected async coreClient(): Promise { - return await new Promise( - async (resolve, reject) => { - const client = await this.coreClientProvider.client(); - if (client && client instanceof Error) { - reject(client); - } else if (client) { - return resolve(client); - } - } - ); + private readonly coreClientProvider: CoreClientProvider; + /** + * Returns with a promise that resolves when the core client is initialized and ready. + */ + protected get coreClient(): Promise { + return this.coreClientProvider.client; } } @@ -326,13 +422,14 @@ ${causes } function isIndexUpdateRequiredBeforeInit( - status: RpcStatus[] + status: RpcStatus[], + cliConfig: DefaultCliConfig ): IndexUpdateRequiredBeforeInitError | undefined { const causes = status .filter((s) => - IndexUpdateRequiredBeforeInit.map((predicate) => predicate(s)).some( - Boolean - ) + IndexUpdateRequiredBeforeInit.map((predicate) => + predicate(s, cliConfig) + ).some(Boolean) ) .map((s) => RpcStatus.toObject(false, s)); return causes.length @@ -343,9 +440,14 @@ const IndexUpdateRequiredBeforeInit = [ isPackageIndexMissingStatus, isDiscoveryNotFoundStatus, ]; -function isPackageIndexMissingStatus(status: RpcStatus): boolean { +function isPackageIndexMissingStatus( + status: RpcStatus, + { directories: { data } }: DefaultCliConfig +): boolean { const predicate = ({ message }: RpcStatus.AsObject) => - message.includes('loading json index file'); + message.includes('loading json index file') && + (message.includes(join(data, 'package_index.json')) || + message.includes(join(data, 'library_index.json'))); // https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247 return evaluate(status, predicate); } diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index c1919d6db..a1dc98b26 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -48,7 +48,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { compilerWarnings?: CompilerWarnings; } ): Promise { - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const handler = this.createOnDataHandler(); const request = this.compileRequest(options, instance); @@ -158,7 +158,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { ): Promise { await this.compile(Object.assign(options, { exportBinaries: false })); - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const request = this.uploadOrUploadUsingProgrammerRequest( options, @@ -228,7 +228,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { } async burnBootloader(options: CoreService.Bootloader.Options): Promise { - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const handler = this.createOnDataHandler(); const request = this.burnBootloaderRequest(options, instance); diff --git a/arduino-ide-extension/src/node/grpc-client-provider.ts b/arduino-ide-extension/src/node/grpc-client-provider.ts deleted file mode 100644 index 10d7b3d70..000000000 --- a/arduino-ide-extension/src/node/grpc-client-provider.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - inject, - injectable, - postConstruct, -} from '@theia/core/shared/inversify'; -import { ILogger } from '@theia/core/lib/common/logger'; -import { MaybePromise } from '@theia/core/lib/common/types'; -import { ConfigServiceImpl } from './config-service-impl'; -import { ArduinoDaemonImpl } from './arduino-daemon-impl'; - -@injectable() -export abstract class GrpcClientProvider { - @inject(ILogger) - protected readonly logger: ILogger; - - @inject(ArduinoDaemonImpl) - protected readonly daemon: ArduinoDaemonImpl; - - @inject(ConfigServiceImpl) - protected readonly configService: ConfigServiceImpl; - - protected _port: string | undefined; - protected _client: C | Error | undefined; - - @postConstruct() - protected init(): void { - this.configService.onConfigChange(() => { - // Only reconcile the gRPC client if the port is known. Hence the CLI daemon is running. - if (this._port) { - this.reconcileClient(this._port); - } - }); - this.daemon.getPort().then((port) => this.reconcileClient(port)); - this.daemon.onDaemonStopped(() => { - if (this._client && !(this._client instanceof Error)) { - this.close(this._client); - } - this._client = undefined; - this._port = undefined; - }); - } - - async client(): Promise { - try { - await this.daemon.getPort(); - return this._client; - } catch (error) { - return error; - } - } - - protected async reconcileClient(port: string): Promise { - this._port = port; - if (this._client && !(this._client instanceof Error)) { - this.close(this._client); - this._client = undefined; - } - try { - const client = await this.createClient(this._port); - this._client = client; - } catch (error) { - this.logger.error('Could not create client for gRPC.', error); - this._client = error; - } - } - - protected abstract createClient(port: string | number): MaybePromise; - - protected abstract close(client: C): void; - - protected get channelOptions(): Record { - const pjson = require('../../package.json') || { version: '0.0.0' }; - return { - 'grpc.max_send_message_length': 512 * 1024 * 1024, - 'grpc.max_receive_message_length': 512 * 1024 * 1024, - 'grpc.primary_user_agent': `arduino-ide/${pjson.version}`, - }; - } -} diff --git a/arduino-ide-extension/src/node/grpc-installable.ts b/arduino-ide-extension/src/node/grpc-installable.ts deleted file mode 100644 index 38d5f67bc..000000000 --- a/arduino-ide-extension/src/node/grpc-installable.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - ProgressMessage, - ResponseService, -} from '../common/protocol/response-service'; -import { - DownloadProgress, - TaskProgress, -} from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; - -export interface InstallResponse { - getProgress?(): DownloadProgress | undefined; - getTaskProgress(): TaskProgress | undefined; -} - -export namespace InstallWithProgress { - export interface Options { - /** - * _unknown_ progress if falsy. - */ - readonly progressId?: string; - readonly responseService: ResponseService; - } - - export function createDataCallback({ - responseService, - progressId, - }: InstallWithProgress.Options): (response: InstallResponse) => void { - let localFile = ''; - let localTotalSize = Number.NaN; - return (response: InstallResponse) => { - const download = response.getProgress - ? response.getProgress() - : undefined; - const task = response.getTaskProgress(); - if (!download && !task) { - throw new Error( - "Implementation error. Neither 'download' nor 'task' is available." - ); - } - if (task && download) { - throw new Error( - "Implementation error. Both 'download' and 'task' are available." - ); - } - if (task) { - const message = task.getName() || task.getMessage(); - if (message) { - if (progressId) { - responseService.reportProgress({ - progressId, - message, - work: { done: Number.NaN, total: Number.NaN }, - }); - } - responseService.appendToOutput({ chunk: `${message}\n` }); - } - } else if (download) { - if (download.getFile() && !localFile) { - localFile = download.getFile(); - } - if (download.getTotalSize() > 0 && Number.isNaN(localTotalSize)) { - localTotalSize = download.getTotalSize(); - } - - // This happens only once per file download. - if (download.getTotalSize() && localFile) { - responseService.appendToOutput({ chunk: `${localFile}\n` }); - } - - if (progressId && localFile) { - let work: ProgressMessage.Work | undefined = undefined; - if (download.getDownloaded() > 0 && !Number.isNaN(localTotalSize)) { - work = { - total: localTotalSize, - done: download.getDownloaded(), - }; - } - responseService.reportProgress({ - progressId, - message: `Downloading ${localFile}`, - work, - }); - } - if (download.getCompleted()) { - // Discard local state. - if (progressId && !Number.isNaN(localTotalSize)) { - responseService.reportProgress({ - progressId, - message: '', - work: { done: Number.NaN, total: Number.NaN }, - }); - } - localFile = ''; - localTotalSize = Number.NaN; - } - } - }; - } -} diff --git a/arduino-ide-extension/src/node/grpc-progressible.ts b/arduino-ide-extension/src/node/grpc-progressible.ts new file mode 100644 index 000000000..0a262dd53 --- /dev/null +++ b/arduino-ide-extension/src/node/grpc-progressible.ts @@ -0,0 +1,278 @@ +import { v4 } from 'uuid'; +import { + ProgressMessage, + ResponseService, +} from '../common/protocol/response-service'; +import { + UpdateCoreLibrariesIndexResponse, + UpdateIndexResponse, + UpdateLibrariesIndexResponse, +} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb'; +import { + DownloadProgress, + TaskProgress, +} from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; +import { + PlatformInstallResponse, + PlatformUninstallResponse, +} from './cli-protocol/cc/arduino/cli/commands/v1/core_pb'; +import { + LibraryInstallResponse, + LibraryUninstallResponse, + ZipLibraryInstallResponse, +} from './cli-protocol/cc/arduino/cli/commands/v1/lib_pb'; + +type LibraryProgressResponse = + | LibraryInstallResponse + | LibraryUninstallResponse + | ZipLibraryInstallResponse; +namespace LibraryProgressResponse { + export function is(response: unknown): response is LibraryProgressResponse { + return ( + response instanceof LibraryInstallResponse || + response instanceof LibraryUninstallResponse || + response instanceof ZipLibraryInstallResponse + ); + } + export function workUnit(response: LibraryProgressResponse): UnitOfWork { + return { + task: response.getTaskProgress(), + ...(response instanceof LibraryInstallResponse && { + download: response.getProgress(), + }), + }; + } +} +type PlatformProgressResponse = + | PlatformInstallResponse + | PlatformUninstallResponse; +namespace PlatformProgressResponse { + export function is(response: unknown): response is PlatformProgressResponse { + return ( + response instanceof PlatformInstallResponse || + response instanceof PlatformUninstallResponse + ); + } + export function workUnit(response: PlatformProgressResponse): UnitOfWork { + return { + task: response.getTaskProgress(), + ...(response instanceof PlatformInstallResponse && { + download: response.getProgress(), + }), + }; + } +} +type IndexProgressResponse = + | UpdateIndexResponse + | UpdateLibrariesIndexResponse + | UpdateCoreLibrariesIndexResponse; +namespace IndexProgressResponse { + export function is(response: unknown): response is IndexProgressResponse { + return ( + response instanceof UpdateIndexResponse || + response instanceof UpdateLibrariesIndexResponse || + response instanceof UpdateCoreLibrariesIndexResponse // not used by the IDE2 but available for full typings compatibility + ); + } + export function workUnit(response: IndexProgressResponse): UnitOfWork { + return { download: response.getDownloadProgress() }; + } +} +export type ProgressResponse = + | LibraryProgressResponse + | PlatformProgressResponse + | IndexProgressResponse; + +interface UnitOfWork { + task?: TaskProgress; + download?: DownloadProgress; +} + +/** + * It's solely a dev thing. Flip it to `true` if you want to debug the progress from the CLI responses. + */ +const DEBUG = false; +export namespace ExecuteWithProgress { + export interface Options { + /** + * _unknown_ progress if falsy. + */ + readonly progressId?: string; + readonly responseService: Partial; + } + + export function createDataCallback({ + responseService, + progressId, + }: ExecuteWithProgress.Options): (response: R) => void { + const uuid = v4(); + let localFile = ''; + let localTotalSize = Number.NaN; + return (response: R) => { + if (DEBUG) { + const json = toJson(response); + if (json) { + console.log(`Progress response [${uuid}]: ${json}`); + } + } + const { task, download } = resolve(response); + if (!download && !task) { + console.warn( + "Implementation error. Neither 'download' nor 'task' is available." + ); + // This is still an API error from the CLI, but IDE2 ignores it. + // Technically, it does not cause an error, but could mess up the progress reporting. + // See an example of an empty object `{}` repose here: https://github.com/arduino/arduino-ide/issues/906#issuecomment-1171145630. + return; + } + if (task && download) { + throw new Error( + "Implementation error. Both 'download' and 'task' are available." + ); + } + if (task) { + const message = task.getName() || task.getMessage(); + if (message) { + if (progressId) { + responseService.reportProgress?.({ + progressId, + message, + work: { done: Number.NaN, total: Number.NaN }, + }); + } + responseService.appendToOutput?.({ chunk: `${message}\n` }); + } + } else if (download) { + if (download.getFile() && !localFile) { + localFile = download.getFile(); + } + if (download.getTotalSize() > 0 && Number.isNaN(localTotalSize)) { + localTotalSize = download.getTotalSize(); + } + + // This happens only once per file download. + if (download.getTotalSize() && localFile) { + responseService.appendToOutput?.({ chunk: `${localFile}\n` }); + } + + if (progressId && localFile) { + let work: ProgressMessage.Work | undefined = undefined; + if (download.getDownloaded() > 0 && !Number.isNaN(localTotalSize)) { + work = { + total: localTotalSize, + done: download.getDownloaded(), + }; + } + responseService.reportProgress?.({ + progressId, + message: `Downloading ${localFile}`, + work, + }); + } + if (download.getCompleted()) { + // Discard local state. + if (progressId && !Number.isNaN(localTotalSize)) { + responseService.reportProgress?.({ + progressId, + message: '', + work: { done: Number.NaN, total: Number.NaN }, + }); + } + localFile = ''; + localTotalSize = Number.NaN; + } + } + }; + } + function resolve(response: unknown): Readonly> { + if (LibraryProgressResponse.is(response)) { + return LibraryProgressResponse.workUnit(response); + } else if (PlatformProgressResponse.is(response)) { + return PlatformProgressResponse.workUnit(response); + } else if (IndexProgressResponse.is(response)) { + return IndexProgressResponse.workUnit(response); + } + console.warn('Unhandled gRPC response', response); + return {}; + } + function toJson(response: ProgressResponse): string | undefined { + if (response instanceof LibraryInstallResponse) { + return JSON.stringify(LibraryInstallResponse.toObject(false, response)); + } else if (response instanceof LibraryUninstallResponse) { + return JSON.stringify(LibraryUninstallResponse.toObject(false, response)); + } else if (response instanceof ZipLibraryInstallResponse) { + return JSON.stringify( + ZipLibraryInstallResponse.toObject(false, response) + ); + } else if (response instanceof PlatformInstallResponse) { + return JSON.stringify(PlatformInstallResponse.toObject(false, response)); + } else if (response instanceof PlatformUninstallResponse) { + return JSON.stringify( + PlatformUninstallResponse.toObject(false, response) + ); + } else if (response instanceof UpdateIndexResponse) { + return JSON.stringify(UpdateIndexResponse.toObject(false, response)); + } else if (response instanceof UpdateLibrariesIndexResponse) { + return JSON.stringify( + UpdateLibrariesIndexResponse.toObject(false, response) + ); + } else if (response instanceof UpdateCoreLibrariesIndexResponse) { + return JSON.stringify( + UpdateCoreLibrariesIndexResponse.toObject(false, response) + ); + } + console.warn('Unhandled gRPC response', response); + return undefined; + } +} + +export class IndexesUpdateProgressHandler { + private done = 0; + private readonly total: number; + readonly progressId: string; + + constructor( + additionalUrlsCount: number, + private readonly onProgress: (progressMessage: ProgressMessage) => void, + private readonly onError?: ({ + progressId, + message, + }: { + progressId: string; + message: string; + }) => void, + private readonly onStart?: (progressId: string) => void, + private readonly onEnd?: (progressId: string) => void + ) { + this.progressId = v4(); + this.total = IndexesUpdateProgressHandler.total(additionalUrlsCount); + // Note: at this point, the IDE2 backend might not have any connected clients, so this notification is not delivered to anywhere + // Hence, clients must handle gracefully when no `willUpdate` is received before any `didProgress`. + this.onStart?.(this.progressId); + } + + reportEnd(): void { + this.onEnd?.(this.progressId); + } + + reportProgress(message: string): void { + this.onProgress({ + message, + progressId: this.progressId, + work: { total: this.total, done: ++this.done }, + }); + } + + reportError(message: string): void { + this.onError?.({ progressId: this.progressId, message }); + } + + private static total(additionalUrlsCount: number): number { + // +1 for the `package_index.tar.bz2` when updating the platform index. + const totalPlatformIndexCount = additionalUrlsCount + 1; + // The `library_index.json.gz` and `library_index.json.sig` when running the library index update. + const totalLibraryIndexCount = 2; + // +1 for the `initInstance` call after the index update (`reportEnd`) + return totalPlatformIndexCount + totalLibraryIndexCount + 1; + } +} diff --git a/arduino-ide-extension/src/node/library-service-server-impl.ts b/arduino-ide-extension/src/node/library-service-impl.ts similarity index 92% rename from arduino-ide-extension/src/node/library-service-server-impl.ts rename to arduino-ide-extension/src/node/library-service-impl.ts index 3841e44d4..42ad456c4 100644 --- a/arduino-ide-extension/src/node/library-service-server-impl.ts +++ b/arduino-ide-extension/src/node/library-service-impl.ts @@ -25,7 +25,7 @@ import { Installable } from '../common/protocol/installable'; import { ILogger, notEmpty } from '@theia/core'; import { FileUri } from '@theia/core/lib/node'; import { ResponseService, NotificationServiceServer } from '../common/protocol'; -import { InstallWithProgress } from './grpc-installable'; +import { ExecuteWithProgress } from './grpc-progressible'; @injectable() export class LibraryServiceImpl @@ -45,8 +45,7 @@ export class LibraryServiceImpl protected readonly notificationServer: NotificationServiceServer; async search(options: { query?: string }): Promise { - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const listReq = new LibraryListRequest(); @@ -112,8 +111,7 @@ export class LibraryServiceImpl }: { fqbn?: string | undefined; }): Promise { - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const req = new LibraryListRequest(); req.setInstance(instance); @@ -218,8 +216,7 @@ export class LibraryServiceImpl version: Installable.Version; filterSelf?: boolean; }): Promise { - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const req = new LibraryResolveDependenciesRequest(); req.setInstance(instance); @@ -260,8 +257,7 @@ export class LibraryServiceImpl const version = !!options.version ? options.version : item.availableVersions[0]; - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const req = new LibraryInstallRequest(); @@ -278,7 +274,7 @@ export class LibraryServiceImpl const resp = client.libraryInstall(req); resp.on( 'data', - InstallWithProgress.createDataCallback({ + ExecuteWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService, }) @@ -304,7 +300,7 @@ export class LibraryServiceImpl const items = await this.search({}); const updated = items.find((other) => LibraryPackage.equals(other, item)) || item; - this.notificationServer.notifyLibraryInstalled({ item: updated }); + this.notificationServer.notifyLibraryDidInstall({ item: updated }); console.info('<<< Library package installation done.', item); } @@ -317,8 +313,7 @@ export class LibraryServiceImpl progressId?: string; overwrite?: boolean; }): Promise { - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const req = new ZipLibraryInstallRequest(); req.setPath(FileUri.fsPath(zipUri)); @@ -333,7 +328,7 @@ export class LibraryServiceImpl const resp = client.zipLibraryInstall(req); resp.on( 'data', - InstallWithProgress.createDataCallback({ + ExecuteWithProgress.createDataCallback({ progressId, responseService: this.responseService, }) @@ -352,8 +347,7 @@ export class LibraryServiceImpl progressId?: string; }): Promise { const { item, progressId } = options; - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const req = new LibraryUninstallRequest(); @@ -369,7 +363,7 @@ export class LibraryServiceImpl const resp = client.libraryUninstall(req); resp.on( 'data', - InstallWithProgress.createDataCallback({ + ExecuteWithProgress.createDataCallback({ progressId, responseService: this.responseService, }) @@ -382,7 +376,7 @@ export class LibraryServiceImpl resp.on('error', reject); }); - this.notificationServer.notifyLibraryUninstalled({ item }); + this.notificationServer.notifyLibraryDidUninstall({ item }); console.info('<<< Library package uninstallation done.', item); } diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index fc458a49c..cb62df7f6 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -317,7 +317,6 @@ export class MonitorManager extends CoreClientAware { board, port, monitorID, - coreClientProvider: this.coreClientProvider, }); this.monitorServices.set(monitorID, monitor); monitor.onDispose( diff --git a/arduino-ide-extension/src/node/monitor-service-factory.ts b/arduino-ide-extension/src/node/monitor-service-factory.ts index 6f88cdb0f..df7a8185b 100644 --- a/arduino-ide-extension/src/node/monitor-service-factory.ts +++ b/arduino-ide-extension/src/node/monitor-service-factory.ts @@ -1,20 +1,13 @@ import { Board, Port } from '../common/protocol'; -import { CoreClientProvider } from './core-client-provider'; import { MonitorService } from './monitor-service'; export const MonitorServiceFactory = Symbol('MonitorServiceFactory'); export interface MonitorServiceFactory { - (options: { - board: Board; - port: Port; - monitorID: string; - coreClientProvider: CoreClientProvider; - }): MonitorService; + (options: { board: Board; port: Port; monitorID: string }): MonitorService; } export interface MonitorServiceFactoryOptions { board: Board; port: Port; monitorID: string; - coreClientProvider: CoreClientProvider; } diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index a0ab86f6f..3eb3c71a8 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -10,7 +10,7 @@ import { MonitorRequest, MonitorResponse, } from './cli-protocol/cc/arduino/cli/commands/v1/monitor_pb'; -import { CoreClientAware, CoreClientProvider } from './core-client-provider'; +import { CoreClientAware } from './core-client-provider'; import { WebSocketProvider } from './web-socket/web-socket-provider'; import { Port as gRPCPort } from 'arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/port_pb'; import { @@ -77,8 +77,7 @@ export class MonitorService extends CoreClientAware implements Disposable { private readonly board: Board, private readonly port: Port, - private readonly monitorID: string, - protected override readonly coreClientProvider: CoreClientProvider + private readonly monitorID: string ) { super(); @@ -175,8 +174,7 @@ export class MonitorService extends CoreClientAware implements Disposable { }, }; - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { instance } = coreClient; const monitorRequest = new MonitorRequest(); @@ -224,7 +222,7 @@ export class MonitorService extends CoreClientAware implements Disposable { async createDuplex(): Promise< ClientDuplexStream > { - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; return coreClient.client.monitor(); } @@ -404,8 +402,7 @@ export class MonitorService extends CoreClientAware implements Disposable { if (!this.duplex) { return Status.NOT_CONNECTED; } - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { instance } = coreClient; const req = new MonitorRequest(); @@ -431,7 +428,7 @@ export class MonitorService extends CoreClientAware implements Disposable { return this.settings; } - // TODO: move this into MonitoSettingsProvider + // TODO: move this into MonitorSettingsProvider /** * Returns the possible configurations used to connect a monitor * to the board specified by fqbn using the specified protocol @@ -443,8 +440,7 @@ export class MonitorService extends CoreClientAware implements Disposable { protocol: string, fqbn: string ): Promise { - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { client, instance } = coreClient; const req = new EnumerateMonitorPortSettingsRequest(); req.setInstance(instance); @@ -512,8 +508,7 @@ export class MonitorService extends CoreClientAware implements Disposable { if (!this.duplex) { return Status.NOT_CONNECTED; } - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); + const coreClient = await this.coreClient; const { instance } = coreClient; const req = new MonitorRequest(); diff --git a/arduino-ide-extension/src/node/notification-service-server.ts b/arduino-ide-extension/src/node/notification-service-server.ts index 851452d38..733edb336 100644 --- a/arduino-ide-extension/src/node/notification-service-server.ts +++ b/arduino-ide-extension/src/node/notification-service-server.ts @@ -1,5 +1,5 @@ import { injectable } from '@theia/core/shared/inversify'; -import { +import type { NotificationServiceServer, NotificationServiceClient, AttachedBoardsChangeEvent, @@ -7,52 +7,79 @@ import { LibraryPackage, Config, Sketch, + ProgressMessage, } from '../common/protocol'; @injectable() export class NotificationServiceServerImpl implements NotificationServiceServer { - protected readonly clients: NotificationServiceClient[] = []; + private readonly clients: NotificationServiceClient[] = []; - notifyIndexUpdated(): void { - this.clients.forEach((client) => client.notifyIndexUpdated()); + notifyIndexWillUpdate(progressId: string): void { + this.clients.forEach((client) => client.notifyIndexWillUpdate(progressId)); } - notifyDaemonStarted(port: string): void { - this.clients.forEach((client) => client.notifyDaemonStarted(port)); + notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void { + this.clients.forEach((client) => + client.notifyIndexUpdateDidProgress(progressMessage) + ); } - notifyDaemonStopped(): void { - this.clients.forEach((client) => client.notifyDaemonStopped()); + notifyIndexDidUpdate(progressId: string): void { + this.clients.forEach((client) => client.notifyIndexDidUpdate(progressId)); } - notifyPlatformInstalled(event: { item: BoardsPackage }): void { - this.clients.forEach((client) => client.notifyPlatformInstalled(event)); + notifyIndexUpdateDidFail({ + progressId, + message, + }: { + progressId: string; + message: string; + }): void { + this.clients.forEach((client) => + client.notifyIndexUpdateDidFail({ progressId, message }) + ); } - notifyPlatformUninstalled(event: { item: BoardsPackage }): void { - this.clients.forEach((client) => client.notifyPlatformUninstalled(event)); + notifyDaemonDidStart(port: string): void { + this.clients.forEach((client) => client.notifyDaemonDidStart(port)); } - notifyLibraryInstalled(event: { item: LibraryPackage }): void { - this.clients.forEach((client) => client.notifyLibraryInstalled(event)); + notifyDaemonDidStop(): void { + this.clients.forEach((client) => client.notifyDaemonDidStop()); } - notifyLibraryUninstalled(event: { item: LibraryPackage }): void { - this.clients.forEach((client) => client.notifyLibraryUninstalled(event)); + notifyPlatformDidInstall(event: { item: BoardsPackage }): void { + this.clients.forEach((client) => client.notifyPlatformDidInstall(event)); } - notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { - this.clients.forEach((client) => client.notifyAttachedBoardsChanged(event)); + notifyPlatformDidUninstall(event: { item: BoardsPackage }): void { + this.clients.forEach((client) => client.notifyPlatformDidUninstall(event)); } - notifyConfigChanged(event: { config: Config | undefined }): void { - this.clients.forEach((client) => client.notifyConfigChanged(event)); + notifyLibraryDidInstall(event: { item: LibraryPackage }): void { + this.clients.forEach((client) => client.notifyLibraryDidInstall(event)); } - notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void { - this.clients.forEach((client) => client.notifyRecentSketchesChanged(event)); + notifyLibraryDidUninstall(event: { item: LibraryPackage }): void { + this.clients.forEach((client) => client.notifyLibraryDidUninstall(event)); + } + + notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void { + this.clients.forEach((client) => + client.notifyAttachedBoardsDidChange(event) + ); + } + + notifyConfigDidChange(event: { config: Config | undefined }): void { + this.clients.forEach((client) => client.notifyConfigDidChange(event)); + } + + notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void { + this.clients.forEach((client) => + client.notifyRecentSketchesDidChange(event) + ); } setClient(client: NotificationServiceClient): void { diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index b2ec97eaf..c7c74724b 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -189,7 +189,7 @@ export class SketchesServiceImpl } async loadSketch(uri: string): Promise { - const { client, instance } = await this.coreClient(); + const { client, instance } = await this.coreClient; const req = new LoadSketchRequest(); const requestSketchPath = FileUri.fsPath(uri); req.setSketchPath(requestSketchPath); @@ -295,7 +295,7 @@ export class SketchesServiceImpl await promisify(fs.writeFile)(fsPath, JSON.stringify(data, null, 2)); this.recentlyOpenedSketches().then((sketches) => - this.notificationService.notifyRecentSketchesChanged({ sketches }) + this.notificationService.notifyRecentSketchesDidChange({ sketches }) ); } @@ -549,9 +549,8 @@ void loop() { } async archive(sketch: Sketch, destinationUri: string): Promise { - await this.coreClientProvider.initialized; await this.loadSketch(sketch.uri); // sanity check - const { client } = await this.coreClient(); + const { client } = await this.coreClient; const archivePath = FileUri.fsPath(destinationUri); // The CLI cannot override existing archives, so we have to wipe it manually: https://github.com/arduino/arduino-cli/issues/1160 if (await promisify(fs.exists)(archivePath)) { diff --git a/arduino-ide-extension/src/test/node/arduino-daemon-impl.test.ts b/arduino-ide-extension/src/test/node/arduino-daemon-impl.test.ts index b0f44b701..af9f66eea 100644 --- a/arduino-ide-extension/src/test/node/arduino-daemon-impl.test.ts +++ b/arduino-ide-extension/src/test/node/arduino-daemon-impl.test.ts @@ -67,52 +67,6 @@ describe('arduino-daemon-impl', () => { track.cleanupSync(); }); - // it('should parse an error - address already in use error [json]', async function (): Promise { - // if (process.platform === 'win32') { - // this.skip(); - // } - // let server: net.Server | undefined = undefined; - // try { - // server = await new Promise(resolve => { - // const server = net.createServer(); - // server.listen(() => resolve(server)); - // }); - // const address = server.address() as net.AddressInfo; - // await new SilentArduinoDaemonImpl(address.port, 'json').spawnDaemonProcess(); - // fail('Expected a failure.') - // } catch (e) { - // expect(e).to.be.instanceOf(DaemonError); - // expect(e.code).to.be.equal(DaemonError.ADDRESS_IN_USE); - // } finally { - // if (server) { - // server.close(); - // } - // } - // }); - - // it('should parse an error - address already in use error [text]', async function (): Promise { - // if (process.platform === 'win32') { - // this.skip(); - // } - // let server: net.Server | undefined = undefined; - // try { - // server = await new Promise(resolve => { - // const server = net.createServer(); - // server.listen(() => resolve(server)); - // }); - // const address = server.address() as net.AddressInfo; - // await new SilentArduinoDaemonImpl(address.port, 'text').spawnDaemonProcess(); - // fail('Expected a failure.') - // } catch (e) { - // expect(e).to.be.instanceOf(DaemonError); - // expect(e.code).to.be.equal(DaemonError.ADDRESS_IN_USE); - // } finally { - // if (server) { - // server.close(); - // } - // } - // }); - it('should parse the port address when the log format is json', async () => { const { daemon, port } = await new SilentArduinoDaemonImpl( 'json' diff --git a/i18n/en.json b/i18n/en.json index d6e7ac07d..3dcee3556 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -129,6 +129,11 @@ "coreContribution": { "copyError": "Copy error messages" }, + "daemon": { + "restart": "Restart Daemon", + "start": "Start Daemon", + "stop": "Stop Daemon" + }, "debug": { "debugWithMessage": "Debug - {0}", "debuggingNotSupported": "Debugging is not supported by '{0}'",