Update package index on 3rd party URLs change.

Closes #637
Closes #906

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2022-06-29 11:14:56 +02:00 committed by Akos Kitta
parent 1073c3fc7d
commit a36524e02a
43 changed files with 1052 additions and 704 deletions

View File

@ -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);

View File

@ -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();
}

View File

@ -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) =>

View File

@ -33,7 +33,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
protected readonly onChangedEmitter = new Emitter<void>();
onStart(): void {
this.notificationCenter.onPlatformInstalled(async ({ item }) => {
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
let shouldFireChanged = false;
for (const fqbn of item.boards
.map(({ fqbn }) => fqbn)

View File

@ -33,10 +33,10 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
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)
),
]);

View File

@ -77,13 +77,13 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
private readonly _reconciled = new Deferred<void>();
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)
);

View File

@ -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;

View File

@ -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()

View File

@ -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',
};
}
}

View File

@ -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<void> {

View File

@ -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<void> {

View File

@ -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()
);
}

View File

@ -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<Progress & { progressId: string }> {
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;
}
}
}

View File

@ -36,7 +36,7 @@ export class OpenRecentSketch extends SketchContribution {
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
override onStart(): void {
this.notificationCenter.onRecentSketchesChanged(({ sketches }) =>
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
this.refreshMenu(sketches)
);
}

View File

@ -41,8 +41,8 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
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)
),
]);

View File

@ -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<void>();
protected readonly daemonStartedEmitter = new Emitter<string>();
protected readonly daemonStoppedEmitter = new Emitter<void>();
protected readonly configChangedEmitter = new Emitter<{
protected readonly indexDidUpdateEmitter = new Emitter<string>();
protected readonly indexWillUpdateEmitter = new Emitter<string>();
protected readonly indexUpdateDidProgressEmitter =
new Emitter<ProgressMessage>();
protected readonly indexUpdateDidFailEmitter = new Emitter<{
progressId: string;
message: string;
}>();
protected readonly daemonDidStartEmitter = new Emitter<string>();
protected readonly daemonDidStopEmitter = new Emitter<void>();
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<AttachedBoardsChangeEvent>();
protected readonly recentSketchesChangedEmitter = new Emitter<{
sketches: Sketch[];
@ -60,27 +68,34 @@ export class NotificationCenter
new Emitter<FrontendApplicationState>();
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);
}
notifyDaemonStarted(port: string): void {
this.daemonStartedEmitter.fire(port);
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void {
this.indexUpdateDidProgressEmitter.fire(progressMessage);
}
notifyDaemonStopped(): void {
this.daemonStoppedEmitter.fire();
notifyIndexDidUpdate(progressId: string): void {
this.indexDidUpdateEmitter.fire(progressId);
}
notifyConfigChanged(event: { config: Config | undefined }): void {
this.configChangedEmitter.fire(event);
notifyIndexUpdateDidFail({
progressId,
message,
}: {
progressId: string;
message: string;
}): void {
this.indexUpdateDidFailEmitter.fire({ progressId, message });
}
notifyPlatformInstalled(event: { item: BoardsPackage }): void {
this.platformInstalledEmitter.fire(event);
notifyDaemonDidStart(port: string): void {
this.daemonDidStartEmitter.fire(port);
}
notifyPlatformUninstalled(event: { item: BoardsPackage }): void {
this.platformUninstalledEmitter.fire(event);
notifyDaemonDidStop(): void {
this.daemonDidStopEmitter.fire();
}
notifyLibraryInstalled(event: { item: LibraryPackage }): void {
this.libraryInstalledEmitter.fire(event);
notifyConfigDidChange(event: { config: Config | undefined }): void {
this.configDidChangeEmitter.fire(event);
}
notifyLibraryUninstalled(event: { item: LibraryPackage }): void {
this.libraryUninstalledEmitter.fire(event);
notifyPlatformDidInstall(event: { item: BoardsPackage }): void {
this.platformDidInstallEmitter.fire(event);
}
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.attachedBoardsChangedEmitter.fire(event);
notifyPlatformDidUninstall(event: { item: BoardsPackage }): void {
this.platformDidUninstallEmitter.fire(event);
}
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
notifyLibraryDidInstall(event: { item: LibraryPackage }): void {
this.libraryDidInstallEmitter.fire(event);
}
notifyLibraryDidUninstall(event: { item: LibraryPackage }): void {
this.libraryDidUninstallEmitter.fire(event);
}
notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void {
this.attachedBoardsDidChangeEmitter.fire(event);
}
notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void {
this.recentSketchesChangedEmitter.fire(event);
}
}

View File

@ -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();
}

View File

@ -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)
);
}

View File

@ -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<string | undefined>;
readonly messageService: MessageService;
readonly responseService: ResponseServiceArduino;
readonly responseService: ResponseServiceClient;
readonly install: ({
item,
progressId,

View File

@ -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)),
]);
}

View File

@ -12,4 +12,7 @@ export interface ArduinoDaemon {
* Otherwise resolves to the CLI daemon port.
*/
tryGetPort(): Promise<string | undefined>;
start(): Promise<string>;
stop(): Promise<void>;
restart(): Promise<string>;
}

View File

@ -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',

View File

@ -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<T extends ArduinoComponent> {
/**
@ -44,7 +44,7 @@ export namespace Installable {
>(options: {
installable: Installable<T>;
messageService: MessageService;
responseService: ResponseServiceArduino;
responseService: ResponseServiceClient;
item: T;
version: Installable.Version;
}): Promise<void> {
@ -66,7 +66,7 @@ export namespace Installable {
>(options: {
installable: Installable<T>;
messageService: MessageService;
responseService: ResponseServiceArduino;
responseService: ResponseServiceClient;
item: T;
}): Promise<void> {
const { item } = options;
@ -86,7 +86,7 @@ export namespace Installable {
export async function doWithProgress(options: {
run: ({ progressId }: { progressId: string }) => Promise<void>;
messageService: MessageService;
responseService: ResponseServiceArduino;
responseService: ResponseServiceClient;
progressText: string;
}): Promise<void> {
return withProgress(
@ -103,7 +103,7 @@ export namespace Installable {
}
);
try {
options.responseService.clearArduinoChannel();
options.responseService.clearOutput();
await options.run({ progressId });
} finally {
toDispose.dispose();

View File

@ -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';

View File

@ -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<string, unknown>;
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<ProgressMessage>;
clearArduinoChannel: () => void;
clearOutput: () => void;
}

View File

@ -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<string>();
protected readonly onDaemonStoppedEmitter = new Emitter<void>();
private readonly toDispose = new DisposableCollection();
private readonly onDaemonStartedEmitter = new Emitter<string>();
private readonly onDaemonStoppedEmitter = new Emitter<void>();
protected _running = false;
protected _port = new Deferred<string>();
protected _execPath: string | undefined;
private _running = false;
private _port = new Deferred<string>();
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<void> {
async start(): Promise<string> {
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<void> {
async stop(): Promise<void> {
this.toDispose.dispose();
}
async restart(): Promise<string> {
return this.start();
}
// Backend only daemon API
get onDaemonStarted(): Event<string> {
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<string>();
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);
}
}

View File

@ -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>(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
);
}
);

View File

@ -55,7 +55,7 @@ export class BoardDiscovery extends CoreClientAware {
@postConstruct()
protected async init(): Promise<void> {
this.coreClient().then((client) => this.startBoardListWatch(client));
this.coreClient.then((client) => this.startBoardListWatch(client));
}
stopBoardListWatch(coreClient: CoreClientProvider.Client): Promise<void> {
@ -181,7 +181,7 @@ export class BoardDiscovery extends CoreClientAware {
};
this._state = newState;
this.notificationService.notifyAttachedBoardsChanged(event);
this.notificationService.notifyAttachedBoardsDidChange(event);
}
});
this.boardWatchDuplex.write(req);

View File

@ -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<BoardDetails | undefined> {
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<BoardWithPackage[]> {
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<BoardUserField[]> {
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<BoardsPackage[]> {
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<void> {
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);
}
}

View File

@ -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(

View File

@ -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<CoreClientProvider.Client> {
export class CoreClientProvider {
@inject(ArduinoDaemonImpl)
private readonly daemon: ArduinoDaemonImpl;
@inject(ConfigServiceImpl)
private readonly configService: ConfigServiceImpl;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
private readonly notificationService: NotificationServiceServer;
protected readonly onClientReadyEmitter = new Emitter<void>();
private ready = new Deferred<void>();
private pending: Deferred<CoreClientProvider.Client> | undefined;
private _client: CoreClientProvider.Client | undefined;
private readonly toDisposeBeforeCreate = new DisposableCollection();
private readonly toDisposeAfterDidCreate = new DisposableCollection();
private readonly onClientReadyEmitter =
new Emitter<CoreClientProvider.Client>();
private readonly onClientReady = this.onClientReadyEmitter.event;
protected _created = new Deferred<void>();
protected _initialized = new Deferred<void>();
get created(): Promise<void> {
return this._created.promise;
}
get initialized(): Promise<void> {
return this._initialized.promise;
}
get onClientReady(): Event<void> {
return this.onClientReadyEmitter.event;
}
close(client: CoreClientProvider.Client): void {
client.client.close();
this._created.reject();
this._initialized.reject();
this._created = new Deferred<void>();
this._initialized = new Deferred<void>();
}
protected override async reconcileClient(port: string): Promise<void> {
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();
@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 tryGetClient(): CoreClientProvider.Client | undefined {
return this._client;
}
get client(): Promise<CoreClientProvider.Client> {
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;
}
/**
* Encapsulates both the gRPC core client creation (`CreateRequest`) and initialization (`InitRequest`).
*/
private async create(port: string): Promise<CoreClientProvider.Client> {
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);
}
/**
* 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<void> {
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;
}
}
}
});
this.daemon.onDaemonStopped(() => {
if (this._client && !(this._client instanceof Error)) {
this.close(this._client);
}
this._client = undefined;
this._port = undefined;
});
private useClient(
client: CoreClientProvider.Client
): CoreClientProvider.Client {
this._client = client;
this.onClientReadyEmitter.fire(this._client);
return this._client;
}
protected async createClient(
port: string | number
private closeClient(): void {
return this.toDisposeBeforeCreate.dispose();
}
private async createClient(
address: string
): Promise<CoreClientProvider.Client> {
// 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<CreateResponse>((resolve, reject) => {
client.create(new CreateRequest(), (err, res: CreateResponse) => {
const instance = await new Promise<Instance>((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<void> {
const initReq = new InitRequest();
initReq.setInstance(instance);
return new Promise<void>((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<CoreClientProvider.Client> {
/**
* 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<void> {
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<void> {
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<void> {
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<void>((resolve, reject) => {
resp.on('error', (error) => {
reject(error);
});
resp.on('end', resolve);
});
private async updateIndex(
client: CoreClientProvider.Client,
progressHandler?: IndexesUpdateProgressHandler
): Promise<void> {
return this.doUpdateIndex(
() =>
client.client.updateIndex(
new UpdateIndexRequest().setInstance(client.instance)
),
progressHandler,
'platform-index'
);
}
protected async updateIndex({
client,
instance,
}: CoreClientProvider.Client): Promise<void> {
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<void>((resolve, reject) => {
updateResp.on('error', reject);
updateResp.on('end', resolve);
});
private async updateLibraryIndex(
client: CoreClientProvider.Client,
progressHandler?: IndexesUpdateProgressHandler
): Promise<void> {
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<R>,
progressHandler?: IndexesUpdateProgressHandler,
task?: string
): Promise<void> {
const progressId = progressHandler?.progressId;
return retry(
() =>
new Promise<void>((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<string, unknown> {
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<CoreClientProvider.Client> {
return await new Promise<CoreClientProvider.Client>(
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<CoreClientProvider.Client> {
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);
}

View File

@ -48,7 +48,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
compilerWarnings?: CompilerWarnings;
}
): Promise<void> {
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<void> {
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<void> {
const coreClient = await this.coreClient();
const coreClient = await this.coreClient;
const { client, instance } = coreClient;
const handler = this.createOnDataHandler();
const request = this.burnBootloaderRequest(options, instance);

View File

@ -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<C> {
@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<C | Error | undefined> {
try {
await this.daemon.getPort();
return this._client;
} catch (error) {
return error;
}
}
protected async reconcileClient(port: string): Promise<void> {
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<C>;
protected abstract close(client: C): void;
protected get channelOptions(): Record<string, unknown> {
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}`,
};
}
}

View File

@ -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;
}
}
};
}
}

View File

@ -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<ResponseService>;
}
export function createDataCallback<R extends ProgressResponse>({
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<Partial<UnitOfWork>> {
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;
}
}

View File

@ -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<LibraryPackage[]> {
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<LibraryPackage[]> {
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<LibraryDependency[]> {
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<void> {
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<void> {
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);
}

View File

@ -317,7 +317,6 @@ export class MonitorManager extends CoreClientAware {
board,
port,
monitorID,
coreClientProvider: this.coreClientProvider,
});
this.monitorServices.set(monitorID, monitor);
monitor.onDispose(

View File

@ -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;
}

View File

@ -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<MonitorRequest, MonitorResponse>
> {
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<PluggableMonitorSettings> {
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();

View File

@ -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 {

View File

@ -189,7 +189,7 @@ export class SketchesServiceImpl
}
async loadSketch(uri: string): Promise<SketchWithDetails> {
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<string> {
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)) {

View File

@ -67,52 +67,6 @@ describe('arduino-daemon-impl', () => {
track.cleanupSync();
});
// it('should parse an error - address already in use error [json]', async function (): Promise<void> {
// if (process.platform === 'win32') {
// this.skip();
// }
// let server: net.Server | undefined = undefined;
// try {
// server = await new Promise<net.Server>(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<void> {
// if (process.platform === 'win32') {
// this.skip();
// }
// let server: net.Server | undefined = undefined;
// try {
// server = await new Promise<net.Server>(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'

View File

@ -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}'",