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

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