mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-16 13:49:28 +00:00
Show 'progress' indicator during verify/upload.
Closes #575 Closes #1175 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
@@ -33,6 +33,12 @@ import { tryParseError } from './cli-error-parser';
|
||||
import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
|
||||
import { firstToUpperCase, notEmpty } from '../common/utils';
|
||||
import { ServiceError } from './service-error';
|
||||
import { ExecuteWithProgress, ProgressResponse } from './grpc-progressible';
|
||||
|
||||
namespace Uploadable {
|
||||
export type Request = UploadRequest | UploadUsingProgrammerRequest;
|
||||
export type Response = UploadResponse | UploadUsingProgrammerResponse;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
@@ -45,27 +51,27 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
@inject(CommandService)
|
||||
private readonly commandService: CommandService;
|
||||
|
||||
async compile(
|
||||
options: CoreService.Compile.Options & {
|
||||
exportBinaries?: boolean;
|
||||
compilerWarnings?: CompilerWarnings;
|
||||
}
|
||||
): Promise<void> {
|
||||
async compile(options: CoreService.Options.Compile): Promise<void> {
|
||||
const coreClient = await this.coreClient;
|
||||
const { client, instance } = coreClient;
|
||||
let buildPath: string | undefined = undefined;
|
||||
const handler = this.createOnDataHandler<CompileResponse>((response) => {
|
||||
const progressHandler = this.createProgressHandler(options);
|
||||
const buildPathHandler = (response: CompileResponse) => {
|
||||
const currentBuildPath = response.getBuildPath();
|
||||
if (!buildPath && currentBuildPath) {
|
||||
if (currentBuildPath) {
|
||||
buildPath = currentBuildPath;
|
||||
} else {
|
||||
if (!!currentBuildPath && currentBuildPath !== buildPath) {
|
||||
if (!!buildPath && currentBuildPath !== buildPath) {
|
||||
throw new Error(
|
||||
`The CLI has already provided a build path: <${buildPath}>, and there is a new build path value: <${currentBuildPath}>.`
|
||||
`The CLI has already provided a build path: <${buildPath}>, and IDE2 received a new build path value: <${currentBuildPath}>.`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
const handler = this.createOnDataHandler<CompileResponse>(
|
||||
progressHandler,
|
||||
buildPathHandler
|
||||
);
|
||||
const request = this.compileRequest(options, instance);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client
|
||||
@@ -132,20 +138,20 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
}
|
||||
|
||||
private compileRequest(
|
||||
options: CoreService.Compile.Options & {
|
||||
options: CoreService.Options.Compile & {
|
||||
exportBinaries?: boolean;
|
||||
compilerWarnings?: CompilerWarnings;
|
||||
},
|
||||
instance: Instance
|
||||
): CompileRequest {
|
||||
const { sketch, board, compilerWarnings } = options;
|
||||
const { sketch, fqbn, compilerWarnings } = options;
|
||||
const sketchUri = sketch.uri;
|
||||
const sketchPath = FileUri.fsPath(sketchUri);
|
||||
const request = new CompileRequest();
|
||||
request.setInstance(instance);
|
||||
request.setSketchPath(sketchPath);
|
||||
if (board?.fqbn) {
|
||||
request.setFqbn(board.fqbn);
|
||||
if (fqbn) {
|
||||
request.setFqbn(fqbn);
|
||||
}
|
||||
if (compilerWarnings) {
|
||||
request.setWarnings(compilerWarnings.toLowerCase());
|
||||
@@ -163,60 +169,44 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
return request;
|
||||
}
|
||||
|
||||
upload(options: CoreService.Upload.Options): Promise<void> {
|
||||
upload(options: CoreService.Options.Upload): Promise<void> {
|
||||
const { usingProgrammer } = options;
|
||||
return this.doUpload(
|
||||
options,
|
||||
() => new UploadRequest(),
|
||||
(client, req) => client.upload(req),
|
||||
(message: string, locations: CoreError.ErrorLocation[]) =>
|
||||
CoreError.UploadFailed(message, locations),
|
||||
'upload'
|
||||
usingProgrammer
|
||||
? new UploadUsingProgrammerRequest()
|
||||
: new UploadRequest(),
|
||||
(client) =>
|
||||
(usingProgrammer ? client.uploadUsingProgrammer : client.upload).bind(
|
||||
client
|
||||
),
|
||||
usingProgrammer
|
||||
? CoreError.UploadUsingProgrammerFailed
|
||||
: CoreError.UploadFailed,
|
||||
`upload${usingProgrammer ? ' using programmer' : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
async uploadUsingProgrammer(
|
||||
options: CoreService.Upload.Options
|
||||
): Promise<void> {
|
||||
return this.doUpload(
|
||||
options,
|
||||
() => new UploadUsingProgrammerRequest(),
|
||||
(client, req) => client.uploadUsingProgrammer(req),
|
||||
(message: string, locations: CoreError.ErrorLocation[]) =>
|
||||
CoreError.UploadUsingProgrammerFailed(message, locations),
|
||||
'upload using programmer'
|
||||
);
|
||||
}
|
||||
|
||||
protected async doUpload(
|
||||
options: CoreService.Upload.Options,
|
||||
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest,
|
||||
responseHandler: (
|
||||
client: ArduinoCoreServiceClient,
|
||||
request: UploadRequest | UploadUsingProgrammerRequest
|
||||
) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
|
||||
errorHandler: (
|
||||
message: string,
|
||||
locations: CoreError.ErrorLocation[]
|
||||
) => ApplicationError<number, CoreError.ErrorLocation[]>,
|
||||
protected async doUpload<
|
||||
REQ extends Uploadable.Request,
|
||||
RESP extends Uploadable.Response
|
||||
>(
|
||||
options: CoreService.Options.Upload,
|
||||
request: REQ,
|
||||
responseFactory: (
|
||||
client: ArduinoCoreServiceClient
|
||||
) => (request: REQ) => ClientReadableStream<RESP>,
|
||||
errorCtor: ApplicationError.Constructor<number, CoreError.ErrorLocation[]>,
|
||||
task: string
|
||||
): Promise<void> {
|
||||
await this.compile({
|
||||
...options,
|
||||
verbose: options.verbose.compile,
|
||||
exportBinaries: false,
|
||||
});
|
||||
|
||||
const coreClient = await this.coreClient;
|
||||
const { client, instance } = coreClient;
|
||||
const request = this.uploadOrUploadUsingProgrammerRequest(
|
||||
options,
|
||||
instance,
|
||||
requestFactory
|
||||
);
|
||||
const handler = this.createOnDataHandler();
|
||||
const progressHandler = this.createProgressHandler(options);
|
||||
const handler = this.createOnDataHandler(progressHandler);
|
||||
const grpcCall = responseFactory(client);
|
||||
return this.notifyUploadWillStart(options).then(() =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
responseHandler(client, request)
|
||||
grpcCall(this.initUploadRequest(request, options, instance))
|
||||
.on('data', handler.onData)
|
||||
.on('error', (error) => {
|
||||
if (!ServiceError.is(error)) {
|
||||
@@ -231,7 +221,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
);
|
||||
this.sendResponse(error.details, OutputMessage.Severity.Error);
|
||||
reject(
|
||||
errorHandler(
|
||||
errorCtor(
|
||||
message,
|
||||
tryParseError({
|
||||
content: handler.stderr,
|
||||
@@ -249,24 +239,23 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
);
|
||||
}
|
||||
|
||||
private uploadOrUploadUsingProgrammerRequest(
|
||||
options: CoreService.Upload.Options,
|
||||
instance: Instance,
|
||||
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest
|
||||
): UploadRequest | UploadUsingProgrammerRequest {
|
||||
const { sketch, board, port, programmer } = options;
|
||||
private initUploadRequest<REQ extends Uploadable.Request>(
|
||||
request: REQ,
|
||||
options: CoreService.Options.Upload,
|
||||
instance: Instance
|
||||
): REQ {
|
||||
const { sketch, fqbn, port, programmer } = options;
|
||||
const sketchPath = FileUri.fsPath(sketch.uri);
|
||||
const request = requestFactory();
|
||||
request.setInstance(instance);
|
||||
request.setSketchPath(sketchPath);
|
||||
if (board?.fqbn) {
|
||||
request.setFqbn(board.fqbn);
|
||||
if (fqbn) {
|
||||
request.setFqbn(fqbn);
|
||||
}
|
||||
request.setPort(this.createPort(port));
|
||||
if (programmer) {
|
||||
request.setProgrammer(programmer.id);
|
||||
}
|
||||
request.setVerbose(options.verbose.upload);
|
||||
request.setVerbose(options.verbose);
|
||||
request.setVerify(options.verify);
|
||||
|
||||
options.userFields.forEach((e) => {
|
||||
@@ -275,10 +264,11 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
return request;
|
||||
}
|
||||
|
||||
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
|
||||
async burnBootloader(options: CoreService.Options.Bootloader): Promise<void> {
|
||||
const coreClient = await this.coreClient;
|
||||
const { client, instance } = coreClient;
|
||||
const handler = this.createOnDataHandler();
|
||||
const progressHandler = this.createProgressHandler(options);
|
||||
const handler = this.createOnDataHandler(progressHandler);
|
||||
const request = this.burnBootloaderRequest(options, instance);
|
||||
return this.notifyUploadWillStart(options).then(() =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
@@ -315,14 +305,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
}
|
||||
|
||||
private burnBootloaderRequest(
|
||||
options: CoreService.Bootloader.Options,
|
||||
options: CoreService.Options.Bootloader,
|
||||
instance: Instance
|
||||
): BurnBootloaderRequest {
|
||||
const { board, port, programmer } = options;
|
||||
const { fqbn, port, programmer } = options;
|
||||
const request = new BurnBootloaderRequest();
|
||||
request.setInstance(instance);
|
||||
if (board?.fqbn) {
|
||||
request.setFqbn(board.fqbn);
|
||||
if (fqbn) {
|
||||
request.setFqbn(fqbn);
|
||||
}
|
||||
request.setPort(this.createPort(port));
|
||||
if (programmer) {
|
||||
@@ -333,8 +323,24 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
return request;
|
||||
}
|
||||
|
||||
private createProgressHandler<R extends ProgressResponse>(
|
||||
options: CoreService.Options.Base
|
||||
): (response: R) => void {
|
||||
// If client did not provide the progress ID, do nothing.
|
||||
if (!options.progressId) {
|
||||
return () => {
|
||||
/* NOOP */
|
||||
};
|
||||
}
|
||||
return ExecuteWithProgress.createDataCallback<R>({
|
||||
progressId: options.progressId,
|
||||
responseService: this.responseService,
|
||||
});
|
||||
}
|
||||
|
||||
private createOnDataHandler<R extends StreamingResponse>(
|
||||
onResponse?: (response: R) => void
|
||||
// TODO: why not creating a composite handler with progress, `build_path`, and out/err stream handlers?
|
||||
...handlers: ((response: R) => void)[]
|
||||
): Disposable & {
|
||||
stderr: Buffer[];
|
||||
onData: (response: R) => void;
|
||||
@@ -347,14 +353,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
}
|
||||
});
|
||||
});
|
||||
const onData = StreamingResponse.createOnDataHandler(
|
||||
const onData = StreamingResponse.createOnDataHandler({
|
||||
stderr,
|
||||
(out, err) => {
|
||||
onData: (out, err) => {
|
||||
buffer.addChunk(out);
|
||||
buffer.addChunk(err, OutputMessage.Severity.Error);
|
||||
},
|
||||
onResponse
|
||||
);
|
||||
handlers,
|
||||
});
|
||||
return {
|
||||
dispose: () => buffer.dispose(),
|
||||
stderr,
|
||||
@@ -391,7 +397,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
|
||||
private mergeSourceOverrides(
|
||||
req: { getSourceOverrideMap(): jspb.Map<string, string> },
|
||||
options: CoreService.Compile.Options
|
||||
options: CoreService.Options.Compile
|
||||
): void {
|
||||
const sketchPath = FileUri.fsPath(options.sketch.uri);
|
||||
for (const uri of Object.keys(options.sourceOverride)) {
|
||||
@@ -422,18 +428,24 @@ type StreamingResponse =
|
||||
namespace StreamingResponse {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function createOnDataHandler<R extends StreamingResponse>(
|
||||
stderr: Uint8Array[],
|
||||
onData: (out: Uint8Array, err: Uint8Array) => void,
|
||||
onResponse?: (response: R) => void
|
||||
options: StreamingResponse.Options<R>
|
||||
): (response: R) => void {
|
||||
return (response: R) => {
|
||||
const out = response.getOutStream_asU8();
|
||||
const err = response.getErrStream_asU8();
|
||||
stderr.push(err);
|
||||
onData(out, err);
|
||||
if (onResponse) {
|
||||
onResponse(response);
|
||||
}
|
||||
options.stderr.push(err);
|
||||
options.onData(out, err);
|
||||
options.handlers?.forEach((handler) => handler(response));
|
||||
};
|
||||
}
|
||||
export interface Options<R extends StreamingResponse> {
|
||||
readonly stderr: Uint8Array[];
|
||||
readonly onData: (out: Uint8Array, err: Uint8Array) => void;
|
||||
/**
|
||||
* Additional request handlers.
|
||||
* For example, when tracing the progress of a task and
|
||||
* collecting the output (out, err) and the `build_path` from the CLI.
|
||||
*/
|
||||
readonly handlers?: ((response: R) => void)[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DownloadProgress,
|
||||
TaskProgress,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
|
||||
import { CompileResponse } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb';
|
||||
import {
|
||||
PlatformInstallResponse,
|
||||
PlatformUninstallResponse,
|
||||
@@ -21,6 +22,11 @@ import {
|
||||
LibraryUninstallResponse,
|
||||
ZipLibraryInstallResponse,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/lib_pb';
|
||||
import {
|
||||
BurnBootloaderResponse,
|
||||
UploadResponse,
|
||||
UploadUsingProgrammerResponse,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
|
||||
|
||||
type LibraryProgressResponse =
|
||||
| LibraryInstallResponse
|
||||
@@ -78,15 +84,62 @@ namespace IndexProgressResponse {
|
||||
return { download: response.getDownloadProgress() };
|
||||
}
|
||||
}
|
||||
/**
|
||||
* These responses have neither `task` nor `progress` property but for the sake of completeness
|
||||
* on typings (from the gRPC API) and UX, these responses represent an indefinite progress.
|
||||
*/
|
||||
type IndefiniteProgressResponse =
|
||||
| UploadResponse
|
||||
| UploadUsingProgrammerResponse
|
||||
| BurnBootloaderResponse;
|
||||
namespace IndefiniteProgressResponse {
|
||||
export function is(
|
||||
response: unknown
|
||||
): response is IndefiniteProgressResponse {
|
||||
return (
|
||||
response instanceof UploadResponse ||
|
||||
response instanceof UploadUsingProgrammerResponse ||
|
||||
response instanceof BurnBootloaderResponse
|
||||
);
|
||||
}
|
||||
}
|
||||
type DefiniteProgressResponse = CompileResponse;
|
||||
namespace DefiniteProgressResponse {
|
||||
export function is(response: unknown): response is DefiniteProgressResponse {
|
||||
return response instanceof CompileResponse;
|
||||
}
|
||||
}
|
||||
type CoreProgressResponse =
|
||||
| DefiniteProgressResponse
|
||||
| IndefiniteProgressResponse;
|
||||
namespace CoreProgressResponse {
|
||||
export function is(response: unknown): response is CoreProgressResponse {
|
||||
return (
|
||||
DefiniteProgressResponse.is(response) ||
|
||||
IndefiniteProgressResponse.is(response)
|
||||
);
|
||||
}
|
||||
export function workUnit(response: CoreProgressResponse): UnitOfWork {
|
||||
if (DefiniteProgressResponse.is(response)) {
|
||||
return { task: response.getProgress() };
|
||||
}
|
||||
return UnitOfWork.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
export type ProgressResponse =
|
||||
| LibraryProgressResponse
|
||||
| PlatformProgressResponse
|
||||
| IndexProgressResponse;
|
||||
| IndexProgressResponse
|
||||
| CoreProgressResponse;
|
||||
|
||||
interface UnitOfWork {
|
||||
task?: TaskProgress;
|
||||
download?: DownloadProgress;
|
||||
}
|
||||
namespace UnitOfWork {
|
||||
export const Unknown: UnitOfWork = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* It's solely a dev thing. Flip it to `true` if you want to debug the progress from the CLI responses.
|
||||
@@ -115,14 +168,28 @@ export namespace ExecuteWithProgress {
|
||||
console.log(`Progress response [${uuid}]: ${json}`);
|
||||
}
|
||||
}
|
||||
const { task, download } = resolve(response);
|
||||
const unitOfWork = resolve(response);
|
||||
const { task, download } = unitOfWork;
|
||||
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.
|
||||
// report a fake unknown progress.
|
||||
if (unitOfWork === UnitOfWork.Unknown && progressId) {
|
||||
if (progressId) {
|
||||
responseService.reportProgress?.({
|
||||
progressId,
|
||||
message: '',
|
||||
work: { done: Number.NaN, total: Number.NaN },
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
// 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.
|
||||
console.warn(
|
||||
"Implementation error. Neither 'download' nor 'task' is available."
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (task && download) {
|
||||
@@ -132,6 +199,7 @@ export namespace ExecuteWithProgress {
|
||||
}
|
||||
if (task) {
|
||||
const message = task.getName() || task.getMessage();
|
||||
const percent = task.getPercent();
|
||||
if (message) {
|
||||
if (progressId) {
|
||||
responseService.reportProgress?.({
|
||||
@@ -141,6 +209,14 @@ export namespace ExecuteWithProgress {
|
||||
});
|
||||
}
|
||||
responseService.appendToOutput?.({ chunk: `${message}\n` });
|
||||
} else if (percent) {
|
||||
if (progressId) {
|
||||
responseService.reportProgress?.({
|
||||
progressId,
|
||||
message,
|
||||
work: { done: percent, total: 100 },
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (download) {
|
||||
if (download.getFile() && !localFile) {
|
||||
@@ -191,38 +267,38 @@ export namespace ExecuteWithProgress {
|
||||
return PlatformProgressResponse.workUnit(response);
|
||||
} else if (IndexProgressResponse.is(response)) {
|
||||
return IndexProgressResponse.workUnit(response);
|
||||
} else if (CoreProgressResponse.is(response)) {
|
||||
return CoreProgressResponse.workUnit(response);
|
||||
}
|
||||
console.warn('Unhandled gRPC response', response);
|
||||
return {};
|
||||
}
|
||||
function toJson(response: ProgressResponse): string | undefined {
|
||||
let object: Record<string, unknown> | undefined = undefined;
|
||||
if (response instanceof LibraryInstallResponse) {
|
||||
return JSON.stringify(LibraryInstallResponse.toObject(false, response));
|
||||
object = LibraryInstallResponse.toObject(false, response);
|
||||
} else if (response instanceof LibraryUninstallResponse) {
|
||||
return JSON.stringify(LibraryUninstallResponse.toObject(false, response));
|
||||
object = LibraryUninstallResponse.toObject(false, response);
|
||||
} else if (response instanceof ZipLibraryInstallResponse) {
|
||||
return JSON.stringify(
|
||||
ZipLibraryInstallResponse.toObject(false, response)
|
||||
);
|
||||
object = ZipLibraryInstallResponse.toObject(false, response);
|
||||
} else if (response instanceof PlatformInstallResponse) {
|
||||
return JSON.stringify(PlatformInstallResponse.toObject(false, response));
|
||||
object = PlatformInstallResponse.toObject(false, response);
|
||||
} else if (response instanceof PlatformUninstallResponse) {
|
||||
return JSON.stringify(
|
||||
PlatformUninstallResponse.toObject(false, response)
|
||||
);
|
||||
object = PlatformUninstallResponse.toObject(false, response);
|
||||
} else if (response instanceof UpdateIndexResponse) {
|
||||
return JSON.stringify(UpdateIndexResponse.toObject(false, response));
|
||||
object = UpdateIndexResponse.toObject(false, response);
|
||||
} else if (response instanceof UpdateLibrariesIndexResponse) {
|
||||
return JSON.stringify(
|
||||
UpdateLibrariesIndexResponse.toObject(false, response)
|
||||
);
|
||||
object = UpdateLibrariesIndexResponse.toObject(false, response);
|
||||
} else if (response instanceof UpdateCoreLibrariesIndexResponse) {
|
||||
return JSON.stringify(
|
||||
UpdateCoreLibrariesIndexResponse.toObject(false, response)
|
||||
);
|
||||
object = UpdateCoreLibrariesIndexResponse.toObject(false, response);
|
||||
} else if (response instanceof CompileResponse) {
|
||||
object = CompileResponse.toObject(false, response);
|
||||
}
|
||||
console.warn('Unhandled gRPC response', response);
|
||||
return undefined;
|
||||
if (!object) {
|
||||
console.warn('Unhandled gRPC response', response);
|
||||
return undefined;
|
||||
}
|
||||
return JSON.stringify(object);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user