ATL-786: Progress indication for install.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta 2021-04-08 15:18:44 +02:00 committed by Akos Kitta
parent 8071298598
commit 9aff90b0af
30 changed files with 557 additions and 219 deletions

View File

@ -44,7 +44,7 @@ import { WorkspaceService } from './theia/workspace/workspace-service';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { OutputService } from '../common/protocol/output-service';
import { ResponseService } from '../common/protocol/response-service';
import { ArduinoPreferences } from './arduino-preferences';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from './contributions/save-as-sketch';
@ -151,8 +151,8 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
@inject(ExecutableService)
protected executableService: ExecutableService;
@inject(OutputService)
protected readonly outputService: OutputService;
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;

View File

@ -120,8 +120,8 @@ import { OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl, Ou
import { ExecutableService, ExecutableServicePath } from '../common/protocol';
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
import { OutputServiceImpl } from './output-service-impl';
import { OutputServicePath, OutputService } from '../common/protocol/output-service';
import { ResponseServiceImpl } from './response-service-impl';
import { ResponseServicePath, ResponseService } from '../common/protocol/response-service';
import { NotificationCenter } from './notification-center';
import { NotificationServicePath, NotificationServiceServer } from '../common/protocol';
import { About } from './contributions/about';
@ -159,6 +159,10 @@ import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco
import { DebugEditorModel } from './theia/debug/debug-editor-model';
import { DebugEditorModelFactory } from '@theia/debug/lib/browser/editor/debug-editor-model';
import { StorageWrapper } from './storage-wrapper';
import { NotificationManager } from './theia/messages/notifications-manager';
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer';
import { NotificationsRenderer } from './theia/messages/notifications-renderer';
const ElementQueries = require('css-element-queries/src/ElementQueries');
@ -383,11 +387,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, ArchiveSketch);
Contribution.configure(bind, AddZipLibrary);
bind(OutputServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, outputService) => {
WebSocketConnectionProvider.createProxy(container, OutputServicePath, outputService);
return outputService;
bind(ResponseServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, responseService) => {
WebSocketConnectionProvider.createProxy(container, ResponseServicePath, responseService);
return responseService;
});
bind(OutputService).toService(OutputServiceImpl);
bind(ResponseService).toService(ResponseServiceImpl);
bind(NotificationCenter).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(NotificationCenter);
@ -439,4 +443,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(StorageWrapper).toSelf().inSingletonScope();
bind(CommandContribution).toService(StorageWrapper);
bind(NotificationManager).toSelf().inSingletonScope();
rebind(TheiaNotificationManager).toService(NotificationManager);
bind(NotificationsRenderer).toSelf().inSingletonScope();
rebind(TheiaNotificationsRenderer).toService(NotificationsRenderer);
});

View File

@ -1,11 +1,12 @@
import { injectable, inject } from 'inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { BoardsService, BoardsPackage } from '../../common/protocol/boards-service';
import { BoardsService, BoardsPackage, Board } from '../../common/protocol/boards-service';
import { BoardsServiceProvider } from './boards-service-provider';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { InstallationProgressDialog } from '../widgets/progress-dialog';
import { BoardsConfig } from './boards-config';
import { Installable } from '../../common/protocol';
import { ResponseServiceImpl } from '../response-service-impl';
/**
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
@ -23,9 +24,15 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ResponseServiceImpl)
protected readonly responseService: ResponseServiceImpl;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
// Workaround for https://github.com/eclipse-theia/theia/issues/9349
protected notifications: Board[] = [];
onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged(this.ensureCoreExists.bind(this));
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
@ -33,22 +40,29 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
protected ensureCoreExists(config: BoardsConfig.Config): void {
const { selectedBoard } = config;
if (selectedBoard) {
if (selectedBoard && !this.notifications.find(board => Board.sameAs(board, selectedBoard))) {
this.notifications.push(selectedBoard);
this.boardsService.search({}).then(packages => {
const candidates = packages
.filter(pkg => BoardsPackage.contains(selectedBoard, pkg))
.filter(({ installable, installedVersion }) => installable && !installedVersion);
for (const candidate of candidates) {
const candidate = candidates[0];
if (candidate) {
// tslint:disable-next-line:max-line-length
this.messageService.info(`The \`"${candidate.name}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, 'Install Manually', 'Yes').then(async answer => {
const index = this.notifications.findIndex(board => Board.sameAs(board, selectedBoard));
if (index !== -1) {
this.notifications.splice(index, 1);
}
if (answer === 'Yes') {
const dialog = new InstallationProgressDialog(candidate.name, candidate.availableVersions[0]);
dialog.open();
try {
await this.boardsService.install({ item: candidate });
} finally {
dialog.close();
}
await Installable.installWithProgress({
installable: this.boardsService,
item: candidate,
messageService: this.messageService,
responseService: this.responseService,
version: candidate.availableVersions[0]
});
return
}
if (answer) {
this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase()));

View File

@ -33,9 +33,14 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
]);
}
async install({ item, version }: { item: BoardsPackage; version: string; }): Promise<void> {
await super.install({ item, version });
this.messageService.info(`Successfully installed platform ${item.name}:${version}.`, { timeout: 3000 });
protected async install({ item, progressId, version }: { item: BoardsPackage, progressId: string, version: string; }): Promise<void> {
await super.install({ item, progressId, version });
this.messageService.info(`Successfully installed platform ${item.name}:${version}`, { timeout: 3000 });
}
protected async uninstall({ item, progressId }: { item: BoardsPackage, progressId: string }): Promise<void> {
await super.uninstall({ item, progressId });
this.messageService.info(`Successfully uninstalled platform ${item.name}:${item.installedVersion}`, { timeout: 3000 });
}
}

View File

@ -1,12 +1,12 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
import { ArduinoMenus } from '../menu/arduino-menus';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import URI from '@theia/core/lib/common/uri';
import { InstallationProgressDialog } from '../widgets/progress-dialog';
import { LibraryService } from '../../common/protocol';
import { ConfirmDialog } from '@theia/core/lib/browser';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ResponseServiceImpl } from '../response-service-impl';
import { Installable, LibraryService } from '../../common/protocol';
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
@injectable()
export class AddZipLibrary extends SketchContribution {
@ -14,6 +14,9 @@ export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
@inject(ResponseServiceImpl)
protected readonly responseService: ResponseServiceImpl;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
@ -69,11 +72,14 @@ export class AddZipLibrary extends SketchContribution {
}
private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
const dialog = new InstallationProgressDialog('Installing library', 'zip');
try {
this.outputChannelManager.getChannel('Arduino').clear();
dialog.open();
await this.libraryService.installZip({ zipUri, overwrite });
await Installable.doWithProgress({
messageService: this.messageService,
progressText: `Processing ${new URI(zipUri).path.base}`,
responseService: this.responseService,
run: () => this.libraryService.installZip({ zipUri, overwrite })
});
this.messageService.info(`Successfully installed library from ${new URI(zipUri).path.base} archive`, { timeout: 3000 });
} catch (error) {
if (error instanceof Error) {
const match = error.message.match(/library (.*?) already installed/);
@ -88,8 +94,6 @@ export class AddZipLibrary extends SketchContribution {
}
this.messageService.error(error.toString());
throw error;
} finally {
dialog.close();
}
}

View File

@ -82,6 +82,7 @@
"colors": {
"list.highlightForeground": "#005c5f",
"list.activeSelectionBackground": "#005c5f",
"progressBar.background": "#005c5f",
"editor.background": "#ffffff",
"editorCursor.foreground": "#434f54",
"editor.foreground": "#434f54",

View File

@ -37,7 +37,7 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
]);
}
protected async install({ item, version }: { item: LibraryPackage, version: Installable.Version }): Promise<void> {
protected async install({ item, progressId, version }: { item: LibraryPackage, progressId: string, version: Installable.Version }): Promise<void> {
const dependencies = await this.service.listDependencies({ item, version, filterSelf: true });
let installDependencies: boolean | undefined = undefined;
if (dependencies.length) {
@ -84,11 +84,16 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
}
if (typeof installDependencies === 'boolean') {
await this.service.install({ item, version, installDependencies });
this.messageService.info(`Successfully installed library ${item.name}:${version}.`, { timeout: 3000 });
await this.service.install({ item, version, progressId, installDependencies });
this.messageService.info(`Successfully installed library ${item.name}:${version}`, { timeout: 3000 });
}
}
protected async uninstall({ item, progressId }: { item: LibraryPackage, progressId: string }): Promise<void> {
await super.uninstall({ item, progressId });
this.messageService.info(`Successfully uninstalled library ${item.name}:${item.installedVersion}`, { timeout: 3000 });
}
}
class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {

View File

@ -1,22 +0,0 @@
import { inject, injectable } from 'inversify';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { OutputService, OutputMessage } from '../common/protocol/output-service';
@injectable()
export class OutputServiceImpl implements OutputService {
@inject(OutputContribution)
protected outputContribution: OutputContribution;
@inject(OutputChannelManager)
protected outputChannelManager: OutputChannelManager;
append(message: OutputMessage): void {
const { chunk } = message;
const channel = this.outputChannelManager.getChannel('Arduino');
channel.show({ preserveFocus: true });
channel.append(chunk);
}
}

View File

@ -0,0 +1,34 @@
import { inject, injectable } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { ResponseService, OutputMessage, ProgressMessage } from '../common/protocol/response-service';
@injectable()
export class ResponseServiceImpl implements ResponseService {
@inject(OutputContribution)
protected outputContribution: OutputContribution;
@inject(OutputChannelManager)
protected outputChannelManager: OutputChannelManager;
protected readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
readonly onProgressDidChange = this.progressDidChangeEmitter.event;
appendToOutput(message: OutputMessage): void {
const { chunk } = message;
const channel = this.outputChannelManager.getChannel('Arduino');
channel.show({ preserveFocus: true });
channel.append(chunk);
}
clearArduinoChannel(): void {
this.outputChannelManager.getChannel('Arduino').clear();
}
reportProgress(progress: ProgressMessage): void {
this.progressDidChangeEmitter.fire(progress);
}
}

View File

@ -67,3 +67,20 @@ button.theia-button.secondary {
button.theia-button.main {
color: var(--theia-button-foreground);
}
/* To make the progress-bar slightly thicker, and use the color from the status bar */
.theia-progress-bar-container {
width: 100%;
height: 4px;
}
.theia-progress-bar {
height: 4px;
width: 3%;
animation: progress-animation 1.3s 0s infinite cubic-bezier(0.645, 0.045, 0.355, 1);
}
.theia-notification-item-progressbar {
height: 4px;
width: 66%;
}

View File

@ -0,0 +1,34 @@
import * as React from 'react';
import { NotificationComponent } from './notification-component';
import { NotificationCenterComponent as TheiaNotificationCenterComponent } from '@theia/messages/lib/browser/notification-center-component'
const PerfectScrollbar = require('react-perfect-scrollbar');
export class NotificationCenterComponent extends TheiaNotificationCenterComponent {
render(): React.ReactNode {
const empty = this.state.notifications.length === 0;
const title = empty ? 'NO NEW NOTIFICATIONS' : 'NOTIFICATIONS';
return (
<div className={`theia-notifications-container theia-notification-center ${this.state.visibilityState === 'center' ? 'open' : 'closed'}`}>
<div className='theia-notification-center-header'>
<div className='theia-notification-center-header-title'>{title}</div>
<div className='theia-notification-center-header-actions'>
<ul className='theia-notification-actions'>
<li className='collapse' title='Hide Notification Center' onClick={this.onHide} />
<li className='clear-all' title='Clear All' onClick={this.onClearAll} />
</ul>
</div>
</div>
<PerfectScrollbar className='theia-notification-list-scroll-container'>
<div className='theia-notification-list'>
{this.state.notifications.map(notification =>
<NotificationComponent key={notification.messageId} notification={notification} manager={this.props.manager} />
)}
</div>
</PerfectScrollbar>
</div>
);
}
}

View File

@ -0,0 +1,62 @@
import * as React from 'react';
import { NotificationComponent as TheiaNotificationComponent } from '@theia/messages/lib/browser/notification-component'
export class NotificationComponent extends TheiaNotificationComponent {
render(): React.ReactNode {
const { messageId, message, type, collapsed, expandable, source, actions } = this.props.notification;
return (<div key={messageId} className='theia-notification-list-item'>
<div className={`theia-notification-list-item-content ${collapsed ? 'collapsed' : ''}`}>
<div className='theia-notification-list-item-content-main'>
<div className={`theia-notification-icon theia-notification-icon-${type}`} />
<div className='theia-notification-message'>
<span dangerouslySetInnerHTML={{ __html: message }} onClick={this.onMessageClick} />
</div>
<ul className='theia-notification-actions'>
{expandable && (
<li className={collapsed ? 'expand' : 'collapse'} title={collapsed ? 'Expand' : 'Collapse'}
data-message-id={messageId} onClick={this.onToggleExpansion} />
)}
{!this.isProgress && (<li className='clear' title='Clear' data-message-id={messageId} onClick={this.onClear} />)}
</ul>
</div>
{(source || !!actions.length) && (
<div className='theia-notification-list-item-content-bottom'>
<div className='theia-notification-source'>
{source && (<span>{source}</span>)}
</div>
<div className='theia-notification-buttons'>
{actions && actions.map((action, index) => (
<button key={messageId + `-action-${index}`} className='theia-button'
data-message-id={messageId} data-action={action}
onClick={this.onAction}>
{action}
</button>
))}
</div>
</div>
)}
</div>
{this.renderProgressBar()}
</div>);
}
private renderProgressBar(): React.ReactNode {
if (!this.isProgress) {
return undefined;
}
if (!Number.isNaN(this.props.notification.progress)) {
return <div className='theia-notification-item-progress'>
<div className='theia-notification-item-progressbar' style={{ width: `${this.props.notification.progress}%` }} />
</div>;
}
return <div className='theia-progress-bar-container'>
<div className='theia-progress-bar' />
</div>
}
private get isProgress(): boolean {
return typeof this.props.notification.progress === 'number';
}
}

View File

@ -0,0 +1,17 @@
import * as React from 'react';
import { NotificationComponent } from './notification-component';
import { NotificationToastsComponent as TheiaNotificationToastsComponent } from '@theia/messages/lib/browser/notification-toasts-component'
export class NotificationToastsComponent extends TheiaNotificationToastsComponent {
render(): React.ReactNode {
return (
<div className={`theia-notifications-container theia-notification-toasts ${this.state.visibilityState === 'toasts' ? 'open' : 'closed'}`}>
<div className='theia-notification-list'>
{this.state.toasts.map(notification => <NotificationComponent key={notification.messageId} notification={notification} manager={this.props.manager} />)}
</div>
</div>
);
}
}

View File

@ -0,0 +1,37 @@
import { injectable } from 'inversify';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import { ProgressMessage, ProgressUpdate } from '@theia/core/lib/common/message-service-protocol';
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
@injectable()
export class NotificationManager extends TheiaNotificationManager {
async reportProgress(messageId: string, update: ProgressUpdate, originalMessage: ProgressMessage, cancellationToken: CancellationToken): Promise<void> {
const notification = this.find(messageId);
if (!notification) {
return;
}
if (cancellationToken.isCancellationRequested) {
this.clear(messageId);
} else {
notification.message = originalMessage.text && update.message ? `${originalMessage.text}: ${update.message}` :
originalMessage.text || update?.message || notification.message;
// Unlike in Theia, we allow resetting the progress monitor to NaN to enforce unknown progress.
const candidate = this.toPlainProgress(update);
notification.progress = typeof candidate === 'number' ? candidate : notification.progress;
}
this.fireUpdatedEvent();
}
protected toPlainProgress(update: ProgressUpdate): number | undefined {
if (!update.work) {
return undefined;
}
if (Number.isNaN(update.work.done) || Number.isNaN(update.work.total)) {
return Number.NaN; // This should trigger the unknown monitor.
}
return Math.min(update.work.done / update.work.total * 100, 100);
}
}

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { injectable } from 'inversify';
import { NotificationCenterComponent } from './notification-center-component';
import { NotificationToastsComponent } from './notification-toasts-component';
import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer';
@injectable()
export class NotificationsRenderer extends TheiaNotificationsRenderer {
protected render(): void {
ReactDOM.render(<div>
<NotificationToastsComponent manager={this.manager} corePreferences={this.corePreferences} />
<NotificationCenterComponent manager={this.manager} />
</div>, this.container);
}
}

View File

@ -3,16 +3,15 @@ import debounce = require('lodash.debounce');
import { Event } from '@theia/core/lib/common/event';
import { CommandService } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import { OutputCommands } from '@theia/output/lib/browser/output-commands';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { Searchable } from '../../../common/protocol/searchable';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { InstallationProgressDialog, UninstallationProgressDialog } from '../progress-dialog';
import { SearchBar } from './search-bar';
import { ListWidget } from './list-widget';
import { ComponentList } from './component-list';
import { ListItemRenderer } from './list-item-renderer';
import { ResponseServiceImpl } from '../../response-service-impl';
export class FilterableListContainer<T extends ArduinoComponent> extends React.Component<FilterableListContainer.Props<T>, FilterableListContainer.State<T>> {
@ -84,20 +83,14 @@ export class FilterableListContainer<T extends ArduinoComponent> extends React.C
}
protected async install(item: T, version: Installable.Version): Promise<void> {
const { install, searchable, itemLabel } = this.props;
const dialog = new InstallationProgressDialog(itemLabel(item), version);
try {
dialog.open();
await this.clearArduinoChannel();
await install({ item, version });
const items = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
} catch (error) {
this.props.messageService.error(error instanceof Error ? error.message : String(error));
throw error;
} finally {
dialog.close();
}
const { install, searchable } = this.props;
await Installable.doWithProgress({
...this.props,
progressText: `Processing ${item.name}:${version}`,
run: ({ progressId }) => install({ item, progressId, version })
});
const items = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
}
protected async uninstall(item: T): Promise<void> {
@ -110,21 +103,14 @@ export class FilterableListContainer<T extends ArduinoComponent> extends React.C
if (!ok) {
return;
}
const { uninstall, searchable, itemLabel } = this.props;
const dialog = new UninstallationProgressDialog(itemLabel(item));
try {
await this.clearArduinoChannel();
dialog.open();
await uninstall({ item });
const items = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
} finally {
dialog.close();
}
}
private async clearArduinoChannel(): Promise<void> {
return this.props.commandService.executeCommand(OutputCommands.CLEAR.id, { name: 'Arduino' });
const { uninstall, searchable } = this.props;
await Installable.doWithProgress({
...this.props,
progressText: `Processing ${item.name}${item.installedVersion ? `:${item.installedVersion}` : ''}`,
run: ({ progressId }) => uninstall({ item, progressId })
});
const items = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
}
}
@ -139,9 +125,10 @@ export namespace FilterableListContainer {
readonly resolveContainer: (element: HTMLElement) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
readonly filterTextChangeEvent: Event<string | undefined>;
readonly install: ({ item, version }: { item: T, version: Installable.Version }) => Promise<void>;
readonly uninstall: ({ item }: { item: T }) => Promise<void>;
readonly messageService: MessageService;
readonly responseService: ResponseServiceImpl;
readonly install: ({ item, progressId, version }: { item: T, progressId: string, version: Installable.Version }) => Promise<void>;
readonly uninstall: ({ item, progressId }: { item: T, progressId: string }) => Promise<void>;
readonly commandService: CommandService;
}

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import { injectable, postConstruct, inject } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { Emitter } from '@theia/core/lib/common/event';
@ -7,12 +8,11 @@ import { MaybePromise } from '@theia/core/lib/common/types';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { CommandService } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import { Installable } from '../../../common/protocol/installable';
import { Searchable } from '../../../common/protocol/searchable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { Installable, Searchable, ArduinoComponent } from '../../../common/protocol';
import { FilterableListContainer } from './filterable-list-container';
import { ListItemRenderer } from './list-item-renderer';
import { NotificationCenter } from '../../notification-center';
import { ResponseServiceImpl } from '../../response-service-impl';
@injectable()
export abstract class ListWidget<T extends ArduinoComponent> extends ReactWidget {
@ -23,6 +23,9 @@ export abstract class ListWidget<T extends ArduinoComponent> extends ReactWidget
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(ResponseServiceImpl)
protected readonly responseService: ResponseServiceImpl;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@ -63,26 +66,31 @@ export abstract class ListWidget<T extends ArduinoComponent> extends ReactWidget
return this.deferredContainer.promise;
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
protected onActivateRequest(message: Message): void {
super.onActivateRequest(message);
(this.focusNode || this.node).focus();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
protected onUpdateRequest(message: Message): void {
super.onUpdateRequest(message);
this.render();
}
protected onResize(message: Widget.ResizeMessage): void {
super.onResize(message);
this.updateScrollBar();
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
this.focusNode = element;
};
protected async install({ item, version }: { item: T, version: Installable.Version }): Promise<void> {
return this.options.installable.install({ item, version });
protected async install({ item, progressId, version }: { item: T, progressId: string, version: Installable.Version }): Promise<void> {
return this.options.installable.install({ item, progressId, version });
}
protected async uninstall({ item }: { item: T }): Promise<void> {
return this.options.installable.uninstall({ item });
protected async uninstall({ item, progressId }: { item: T, progressId: string }): Promise<void> {
return this.options.installable.uninstall({ item, progressId });
}
render(): React.ReactNode {
@ -97,7 +105,8 @@ export abstract class ListWidget<T extends ArduinoComponent> extends ReactWidget
itemRenderer={this.options.itemRenderer}
filterTextChangeEvent={this.filterTextChangeEmitter.event}
messageService={this.messageService}
commandService={this.commandService} />;
commandService={this.commandService}
responseService={this.responseService} />;
}
/**

View File

@ -1,23 +0,0 @@
import { AbstractDialog } from '@theia/core/lib/browser';
export class InstallationProgressDialog extends AbstractDialog<undefined> {
readonly value = undefined;
constructor(componentName: string, version: string) {
super({ title: 'Installation in progress' });
this.contentNode.textContent = `Installing ${componentName} [${version}]. Please wait...`;
}
}
export class UninstallationProgressDialog extends AbstractDialog<undefined> {
readonly value = undefined;
constructor(componentName: string) {
super({ title: 'Uninstallation in progress' });
this.contentNode.textContent = `Uninstalling ${componentName}. Please wait...`;
}
}

View File

@ -11,5 +11,5 @@ export * from './searchable';
export * from './sketches-service';
export * from './examples-service';
export * from './executable-service';
export * from './output-service';
export * from './response-service';
export * from './notification-service';

View File

@ -1,20 +1,26 @@
import * as semver from 'semver';
import { 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 { ResponseServiceImpl } from '../../browser/response-service-impl';
export interface Installable<T extends ArduinoComponent> {
/**
* If `options.version` is specified, that will be installed. Otherwise, `item.availableVersions[0]`.
*/
install(options: { item: T, version?: Installable.Version }): Promise<void>;
install(options: { item: T, progressId?: string, version?: Installable.Version }): Promise<void>;
/**
* Uninstalls the given component. It is a NOOP if not installed.
*/
uninstall(options: { item: T }): Promise<void>;
uninstall(options: { item: T, progressId?: string }): Promise<void>;
}
export namespace Installable {
export type Version = string;
export namespace Version {
/**
* Most recent version comes first, then the previous versions. (`1.8.1`, `1.6.3`, `1.6.2`, `1.6.1` and so on.)
@ -26,4 +32,70 @@ export namespace Installable {
return naturalCompare(left, right);
};
}
export async function installWithProgress<T extends ArduinoComponent>(options: {
installable: Installable<T>,
messageService: MessageService,
responseService: ResponseServiceImpl,
item: T,
version: Installable.Version
}): Promise<void> {
const { item, version } = options;
return doWithProgress({
...options,
progressText: `Processing ${item.name}:${version}`,
run: ({ progressId }) => options.installable.install({ item: options.item, version: options.version, progressId })
});
}
export async function uninstallWithProgress<T extends ArduinoComponent>(options: {
installable: Installable<T>,
messageService: MessageService,
responseService: ResponseServiceImpl,
item: T
}): Promise<void> {
const { item } = options;
return doWithProgress({
...options,
progressText: `Processing ${item.name}${item.installedVersion ? `:${item.installedVersion}` : ''}`,
run: ({ progressId }) => options.installable.uninstall({ item: options.item, progressId })
});
}
export async function doWithProgress(options: {
run: ({ progressId }: { progressId: string }) => Promise<void>,
messageService: MessageService,
responseService: ResponseServiceImpl,
progressText: string
}): Promise<void> {
return withProgress(options.progressText, options.messageService, async (progress, _) => {
const progressId = progress.id;
const toDispose = options.responseService.onProgressDidChange(progressMessage => {
if (progressId === progressMessage.progressId) {
const { message, work } = progressMessage;
progress.report({ message, work });
}
});
try {
options.responseService.clearArduinoChannel();
await options.run({ progressId });
} finally {
toDispose.dispose();
}
});
}
async function withProgress(text: string, messageService: MessageService, cb: (progress: Progress, token: CancellationToken) => Promise<void>): Promise<void> {
const cancellationSource = new CancellationTokenSource();
const { token } = cancellationSource;
const progress = await messageService.showProgress({ text, options: { cancelable: false } }, () => cancellationSource.cancel());
try {
await cb(progress, token);
} finally {
progress.cancel();
}
}
}

View File

@ -9,8 +9,8 @@ export interface LibraryService extends Installable<LibraryPackage>, Searchable<
/**
* When `installDependencies` is not set, it is `true` by default. If you want to skip the installation of required dependencies, set it to `false`.
*/
install(options: { item: LibraryPackage, version?: Installable.Version, installDependencies?: boolean }): Promise<void>;
installZip(options: { zipUri: string, overwrite?: boolean }): Promise<void>;
install(options: { item: LibraryPackage, progressId?: string, version?: Installable.Version, installDependencies?: boolean }): Promise<void>;
installZip(options: { zipUri: string, progressId?: string, overwrite?: boolean }): Promise<void>;
/**
* Set `filterSelf` to `true` if you want to avoid having `item` in the result set.
* Note: as of today (22.02.2021), the CLI works like this: `./arduino-cli lib deps Adaino@0.1.0 ✕ Adaino 0.1.0 must be installed.`.

View File

@ -1,10 +0,0 @@
export interface OutputMessage {
readonly chunk: string;
readonly severity?: 'error' | 'warning' | 'info'; // Currently not used!
}
export const OutputServicePath = '/services/output-service';
export const OutputService = Symbol('OutputService');
export interface OutputService {
append(message: OutputMessage): void;
}

View File

@ -0,0 +1,23 @@
export interface OutputMessage {
readonly chunk: string;
readonly severity?: 'error' | 'warning' | 'info'; // Currently not used!
}
export interface ProgressMessage {
readonly progressId: string;
readonly message: string;
readonly work?: ProgressMessage.Work;
}
export namespace ProgressMessage {
export interface Work {
readonly done: number;
readonly total: number;
}
}
export const ResponseServicePath = '/services/response-service';
export const ResponseService = Symbol('ResponseService');
export interface ResponseService {
appendToOutput(message: OutputMessage): void;
reportProgress(message: ProgressMessage): void;
}

View File

@ -29,7 +29,7 @@ import { ExamplesServiceImpl } from './examples-service-impl';
import { ExamplesService, ExamplesServicePath } from '../common/protocol/examples-service';
import { ExecutableService, ExecutableServicePath } from '../common/protocol/executable-service';
import { ExecutableServiceImpl } from './executable-service-impl';
import { OutputServicePath, OutputService } from '../common/protocol/output-service';
import { ResponseServicePath, ResponseService } from '../common/protocol/response-service';
import { NotificationServiceServerImpl } from './notification-service-server';
import { NotificationServiceServer, NotificationServiceClient, NotificationServicePath } from '../common/protocol';
import { BackendApplication } from './theia/core/backend-application';
@ -127,7 +127,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Output service per connection.
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bindFrontendService }) => {
bindFrontendService(OutputServicePath, OutputService);
bindFrontendService(ResponseServicePath, ResponseService);
}));
// Notify all connected frontend instances

View File

@ -4,17 +4,18 @@ import { notEmpty } from '@theia/core/lib/common/objects';
import {
BoardsService,
Installable,
BoardsPackage, Board, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer, OutputService, NotificationServiceServer, AvailablePorts, BoardWithPackage
BoardsPackage, Board, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer, ResponseService, NotificationServiceServer, AvailablePorts, BoardWithPackage
} from '../common/protocol';
import {
PlatformInstallRequest, PlatformInstallResponse, PlatformListRequest, PlatformListResponse, PlatformSearchRequest,
PlatformSearchResponse, PlatformUninstallRequest, PlatformUninstallResponse
PlatformInstallRequest, PlatformListRequest, PlatformListResponse, PlatformSearchRequest,
PlatformSearchResponse, PlatformUninstallRequest
} from './cli-protocol/cc/arduino/cli/commands/v1/core_pb';
import { Platform } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
import { BoardDiscovery } from './board-discovery';
import { CoreClientAware } from './core-client-provider';
import { BoardDetailsRequest, BoardDetailsResponse, BoardSearchRequest } from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
import { ListProgrammersAvailableForUploadRequest, ListProgrammersAvailableForUploadResponse } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
import { InstallWithProgress } from './grpc-installable';
@injectable()
export class BoardsServiceImpl extends CoreClientAware implements BoardsService {
@ -26,8 +27,8 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
@named('discovery')
protected discoveryLogger: ILogger;
@inject(OutputService)
protected readonly outputService: OutputService;
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
@ -254,7 +255,7 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
return [...packages.values()];
}
async install(options: { item: BoardsPackage, version?: Installable.Version }): Promise<void> {
async install(options: { item: BoardsPackage, progressId?: string, version?: Installable.Version }): Promise<void> {
const item = options.item;
const version = !!options.version ? options.version : item.availableVersions[0];
const coreClient = await this.coreClient();
@ -270,17 +271,12 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
console.info('>>> Starting boards package installation...', item);
const resp = client.platformInstall(req);
resp.on('data', (r: PlatformInstallResponse) => {
const prog = r.getProgress();
if (prog && prog.getFile()) {
this.outputService.append({ chunk: `downloading ${prog.getFile()}\n` });
}
});
resp.on('data', InstallWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService }));
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', error => {
this.outputService.append({ chunk: `Failed to install platform: ${item.id}.\n` });
this.outputService.append({ chunk: error.toString() });
this.responseService.appendToOutput({ chunk: `Failed to install platform: ${item.id}.\n` });
this.responseService.appendToOutput({ chunk: error.toString() });
reject(error);
});
});
@ -291,8 +287,8 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
console.info('<<< Boards package installation done.', item);
}
async uninstall(options: { item: BoardsPackage }): Promise<void> {
const item = options.item;
async uninstall(options: { item: BoardsPackage, progressId?: string }): Promise<void> {
const { item, progressId } = options;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
@ -304,14 +300,8 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
req.setPlatformPackage(platform);
console.info('>>> Starting boards package uninstallation...', item);
let logged = false;
const resp = client.platformUninstall(req);
resp.on('data', (_: PlatformUninstallResponse) => {
if (!logged) {
this.outputService.append({ chunk: `uninstalling ${item.id}\n` });
logged = true;
}
})
resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService }));
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);

View File

@ -57,9 +57,7 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
if (!instance) {
throw new Error('Could not retrieve instance from the initialize response.');
}
// No `await`. The index update event comes later. This way we do not block app startup with index update when invalid proxy is given.
this.updateIndexes({ instance, client });
await this.updateIndexes({ instance, client });
return { instance, client };
}

View File

@ -2,22 +2,22 @@ import { FileUri } from '@theia/core/lib/node/file-uri';
import { inject, injectable } from 'inversify';
import { relative } from 'path';
import * as jspb from 'google-protobuf';
import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb';
import { ClientReadableStream } from '@grpc/grpc-js';
import { CompilerWarnings, CoreService } from '../common/protocol/core-service';
import { CompileRequest, CompileResponse } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb';
import { CoreClientAware } from './core-client-provider';
import { BurnBootloaderRequest, BurnBootloaderResponse, UploadRequest, UploadResponse, UploadUsingProgrammerRequest, UploadUsingProgrammerResponse } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
import { OutputService } from '../common/protocol/output-service';
import { ResponseService } from '../common/protocol/response-service';
import { NotificationServiceServer } from '../common/protocol';
import { ClientReadableStream } from '@grpc/grpc-js';
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { firstToUpperCase, firstToLowerCase } from '../common/utils';
import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb';
@injectable()
export class CoreServiceImpl extends CoreClientAware implements CoreService {
@inject(OutputService)
protected readonly outputService: OutputService;
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
@ -53,15 +53,15 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (cr: CompileResponse) => {
this.outputService.append({ chunk: Buffer.from(cr.getOutStream_asU8()).toString() });
this.outputService.append({ chunk: Buffer.from(cr.getErrStream_asU8()).toString() });
this.responseService.appendToOutput({ chunk: Buffer.from(cr.getOutStream_asU8()).toString() });
this.responseService.appendToOutput({ chunk: Buffer.from(cr.getErrStream_asU8()).toString() });
});
result.on('error', error => reject(error));
result.on('end', () => resolve());
});
this.outputService.append({ chunk: '\n--------------------------\nCompilation complete.\n' });
this.responseService.appendToOutput({ chunk: '\n--------------------------\nCompilation complete.\n' });
} catch (e) {
this.outputService.append({ chunk: `Compilation error: ${e}\n`, severity: 'error' });
this.responseService.appendToOutput({ chunk: `Compilation error: ${e}\n`, severity: 'error' });
throw e;
}
}
@ -107,15 +107,15 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (resp: UploadResponse) => {
this.outputService.append({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() });
this.outputService.append({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() });
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() });
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() });
});
result.on('error', error => reject(error));
result.on('end', () => resolve());
});
this.outputService.append({ chunk: '\n--------------------------\n' + firstToLowerCase(task) + ' complete.\n' });
this.responseService.appendToOutput({ chunk: '\n--------------------------\n' + firstToLowerCase(task) + ' complete.\n' });
} catch (e) {
this.outputService.append({ chunk: `${firstToUpperCase(task)} error: ${e}\n`, severity: 'error' });
this.responseService.appendToOutput({ chunk: `${firstToUpperCase(task)} error: ${e}\n`, severity: 'error' });
throw e;
}
}
@ -141,14 +141,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (resp: BurnBootloaderResponse) => {
this.outputService.append({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() });
this.outputService.append({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() });
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() });
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() });
});
result.on('error', error => reject(error));
result.on('end', () => resolve());
});
} catch (e) {
this.outputService.append({ chunk: `Error while burning the bootloader: ${e}\n`, severity: 'error' });
this.responseService.appendToOutput({ chunk: `Error while burning the bootloader: ${e}\n`, severity: 'error' });
throw e;
}
}

View File

@ -24,7 +24,7 @@ export abstract class GrpcClientProvider<C> {
const updateClient = () => {
const cliConfig = this.configService.cliConfiguration;
this.reconcileClient(cliConfig ? cliConfig.daemon.port : undefined);
}
};
this.configService.onConfigChange(updateClient);
this.daemon.ready.then(updateClient);
this.daemon.onDaemonStopped(() => {
@ -33,7 +33,7 @@ export abstract class GrpcClientProvider<C> {
}
this._client = undefined;
this._port = undefined;
})
});
}
async client(): Promise<C | Error | undefined> {

View File

@ -0,0 +1,72 @@
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

@ -2,14 +2,15 @@ import { injectable, inject } from 'inversify';
import { LibraryDependency, LibraryLocation, LibraryPackage, LibraryService } from '../common/protocol/library-service';
import { CoreClientAware } from './core-client-provider';
import {
InstalledLibrary, Library, LibraryInstallRequest, LibraryInstallResponse, LibraryListRequest, LibraryListResponse, LibraryLocation as GrpcLibraryLocation, LibraryRelease,
LibraryResolveDependenciesRequest, LibraryUninstallRequest, LibraryUninstallResponse, ZipLibraryInstallRequest, ZipLibraryInstallResponse, LibrarySearchRequest,
InstalledLibrary, Library, LibraryInstallRequest, LibraryListRequest, LibraryListResponse, LibraryLocation as GrpcLibraryLocation, LibraryRelease,
LibraryResolveDependenciesRequest, LibraryUninstallRequest, ZipLibraryInstallRequest, LibrarySearchRequest,
LibrarySearchResponse
} from './cli-protocol/cc/arduino/cli/commands/v1/lib_pb';
import { Installable } from '../common/protocol/installable';
import { ILogger, notEmpty } from '@theia/core';
import { FileUri } from '@theia/core/lib/node';
import { OutputService, NotificationServiceServer } from '../common/protocol';
import { ResponseService, NotificationServiceServer } from '../common/protocol';
import { InstallWithProgress } from './grpc-installable';
@injectable()
export class LibraryServiceImpl extends CoreClientAware implements LibraryService {
@ -17,8 +18,8 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
@inject(ILogger)
protected logger: ILogger;
@inject(OutputService)
protected readonly outputService: OutputService;
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(NotificationServiceServer)
protected readonly notificationServer: NotificationServiceServer;
@ -157,7 +158,7 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
return filterSelf ? dependencies.filter(({ name }) => name !== item.name) : dependencies;
}
async install(options: { item: LibraryPackage, version?: Installable.Version, installDependencies?: boolean }): Promise<void> {
async install(options: { item: LibraryPackage, progressId?: string, version?: Installable.Version, installDependencies?: boolean }): Promise<void> {
const item = options.item;
const version = !!options.version ? options.version : item.availableVersions[0];
const coreClient = await this.coreClient();
@ -173,17 +174,12 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
console.info('>>> Starting library package installation...', item);
const resp = client.libraryInstall(req);
resp.on('data', (r: LibraryInstallResponse) => {
const prog = r.getProgress();
if (prog) {
this.outputService.append({ chunk: `downloading ${prog.getFile()}: ${prog.getCompleted()}%\n` });
}
});
resp.on('data', InstallWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService }));
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', error => {
this.outputService.append({ chunk: `Failed to install library: ${item.name}${version ? `:${version}` : ''}.\n` });
this.outputService.append({ chunk: error.toString() });
this.responseService.appendToOutput({ chunk: `Failed to install library: ${item.name}${version ? `:${version}` : ''}.\n` });
this.responseService.appendToOutput({ chunk: error.toString() });
reject(error);
});
});
@ -194,7 +190,7 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
console.info('<<< Library package installation done.', item);
}
async installZip({ zipUri, overwrite }: { zipUri: string, overwrite?: boolean }): Promise<void> {
async installZip({ zipUri, progressId, overwrite }: { zipUri: string, progressId?: string, overwrite?: boolean }): Promise<void> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new ZipLibraryInstallRequest();
@ -204,20 +200,15 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
req.setOverwrite(overwrite);
}
const resp = client.zipLibraryInstall(req);
resp.on('data', (r: ZipLibraryInstallResponse) => {
const task = r.getTaskProgress();
if (task && task.getMessage()) {
this.outputService.append({ chunk: task.getMessage() });
}
});
resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService }));
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);
});
}
async uninstall(options: { item: LibraryPackage }): Promise<void> {
const item = options.item;
async uninstall(options: { item: LibraryPackage, progressId?: string }): Promise<void> {
const { item, progressId } = options;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
@ -227,14 +218,8 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
req.setVersion(item.installedVersion!);
console.info('>>> Starting library package uninstallation...', item);
let logged = false;
const resp = client.libraryUninstall(req);
resp.on('data', (_: LibraryUninstallResponse) => {
if (!logged) {
this.outputService.append({ chunk: `uninstalling ${item.name}:${item.installedVersion}%\n` });
logged = true;
}
});
resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService }));
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);