Skip to content

Commit 1e7fbf1

Browse files
Merge branch 'main' into 1032-uploads-failing-with-monitor-open
2 parents 01604ee + a9aac0d commit 1e7fbf1

File tree

5 files changed

+199
-86
lines changed

5 files changed

+199
-86
lines changed

arduino-ide-extension/src/browser/theia/core/application-shell.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -130,5 +130,5 @@ DockPanel.prototype.handleEvent = function (event) {
130130
case 'p-drop':
131131
return;
132132
}
133-
originalHandleEvent(event);
133+
originalHandleEvent.bind(this)(event);
134134
};

arduino-ide-extension/src/browser/theia/preferences/preference-tree-generator.ts

+31-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,43 @@
1+
import {
2+
FrontendApplicationState,
3+
FrontendApplicationStateService,
4+
} from '@theia/core/lib/browser/frontend-application-state';
15
import { CompositeTreeNode } from '@theia/core/lib/browser/tree/tree';
2-
import { injectable } from '@theia/core/shared/inversify';
6+
import { inject, injectable } from '@theia/core/shared/inversify';
37
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
48

59
@injectable()
610
export class PreferenceTreeGenerator extends TheiaPreferenceTreeGenerator {
11+
private shouldHandleChangedSchemaOnReady = false;
12+
private state: FrontendApplicationState | undefined;
13+
14+
@inject(FrontendApplicationStateService)
15+
private readonly appStateService: FrontendApplicationStateService;
16+
717
protected override async init(): Promise<void> {
8-
// The IDE2 does not use the default Theia preferences UI.
9-
// There is no need to create and keep the the tree model synchronized when there is no UI for it.
18+
this.appStateService.onStateChanged((state) => {
19+
this.state = state;
20+
// manually trigger a model (and UI) refresh if it was requested during the startup phase.
21+
if (this.state === 'ready' && this.shouldHandleChangedSchemaOnReady) {
22+
this.doHandleChangedSchema();
23+
}
24+
});
25+
return super.init();
26+
}
27+
28+
override doHandleChangedSchema(): void {
29+
if (this.state === 'ready') {
30+
super.doHandleChangedSchema();
31+
}
32+
// don't do anything until the app is `ready`, then invoke `doHandleChangedSchema`.
33+
this.shouldHandleChangedSchemaOnReady = true;
1034
}
1135

12-
// Just returns with the empty root.
1336
override generateTree(): CompositeTreeNode {
37+
if (this.state === 'ready') {
38+
return super.generateTree();
39+
}
40+
// always create an empty root when the app is not ready.
1441
this._root = this.createRootNode();
1542
return this._root;
1643
}

arduino-ide-extension/src/node/core-client-provider.ts

+83-57
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import {
2121
import * as commandsGrpcPb from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
2222
import { NotificationServiceServer } from '../common/protocol';
2323
import { Deferred, retry } from '@theia/core/lib/common/promise-util';
24-
import { Status as RpcStatus } from './cli-protocol/google/rpc/status_pb';
24+
import {
25+
Status as RpcStatus,
26+
Status,
27+
} from './cli-protocol/google/rpc/status_pb';
2528

2629
@injectable()
2730
export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Client> {
@@ -90,10 +93,11 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
9093
this._initialized.resolve();
9194
this.updateIndex(this._client); // Update the indexes asynchronously
9295
} catch (error: unknown) {
93-
if (
94-
this.isPackageIndexMissingError(error) ||
95-
this.isDiscoveryNotFoundError(error)
96-
) {
96+
console.error(
97+
'Error occurred while initializing the core gRPC client provider',
98+
error
99+
);
100+
if (error instanceof IndexUpdateRequiredBeforeInitError) {
97101
// If it's a first start, IDE2 must run index update before the init request.
98102
await this.updateIndexes(this._client);
99103
await this.initInstance(this._client);
@@ -114,41 +118,6 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
114118
});
115119
}
116120

117-
private isPackageIndexMissingError(error: unknown): boolean {
118-
const assert = (message: string) =>
119-
message.includes('loading json index file');
120-
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247
121-
return this.isRpcStatusError(error, assert);
122-
}
123-
124-
private isDiscoveryNotFoundError(error: unknown): boolean {
125-
const assert = (message: string) =>
126-
message.includes('discovery') &&
127-
(message.includes('not found') || message.includes('not installed'));
128-
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L740
129-
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L744
130-
return this.isRpcStatusError(error, assert);
131-
}
132-
133-
private isCancelError(error: unknown): boolean {
134-
return (
135-
error instanceof Error &&
136-
error.message.toLocaleLowerCase().includes('cancelled on client')
137-
);
138-
}
139-
140-
// Final error codes are not yet defined by the CLI. Hence, we do string matching in the message RPC status.
141-
private isRpcStatusError(
142-
error: unknown,
143-
assert: (message: string) => boolean
144-
) {
145-
if (error instanceof RpcStatus) {
146-
const { message } = RpcStatus.toObject(false, error);
147-
return assert(message.toLocaleLowerCase());
148-
}
149-
return false;
150-
}
151-
152121
protected async createClient(
153122
port: string | number
154123
): Promise<CoreClientProvider.Client> {
@@ -192,7 +161,7 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
192161
initReq.setInstance(instance);
193162
return new Promise<void>((resolve, reject) => {
194163
const stream = client.init(initReq);
195-
const errorStatus: RpcStatus[] = [];
164+
const errors: RpcStatus[] = [];
196165
stream.on('data', (res: InitResponse) => {
197166
const progress = res.getInitProgress();
198167
if (progress) {
@@ -210,28 +179,30 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
210179

211180
const error = res.getError();
212181
if (error) {
213-
console.error(error.getMessage());
214-
errorStatus.push(error);
215-
// Cancel the init request. No need to wait until the end of the event. The init has already failed.
216-
// Canceling the request will result in a cancel error, but we need to reject with the original error later.
217-
stream.cancel();
182+
const { code, message } = Status.toObject(false, error);
183+
console.error(
184+
`Detected an error response during the gRPC core client initialization: code: ${code}, message: ${message}`
185+
);
186+
errors.push(error);
218187
}
219188
});
220-
stream.on('error', (error) => {
221-
// On any error during the init request, the request is canceled.
222-
// On cancel, the IDE2 ignores the cancel error and rejects with the original one.
223-
reject(
224-
this.isCancelError(error) && errorStatus.length
225-
? errorStatus[0]
226-
: error
227-
);
189+
stream.on('error', reject);
190+
stream.on('end', () => {
191+
const error = this.evaluateErrorStatus(errors);
192+
if (error) {
193+
reject(error);
194+
return;
195+
}
196+
resolve();
228197
});
229-
stream.on('end', () =>
230-
errorStatus.length ? reject(errorStatus) : resolve()
231-
);
232198
});
233199
}
234200

201+
private evaluateErrorStatus(status: RpcStatus[]): Error | undefined {
202+
const error = isIndexUpdateRequiredBeforeInit(status); // put future error matching here
203+
return error;
204+
}
205+
235206
protected async updateIndexes(
236207
client: CoreClientProvider.Client
237208
): Promise<CoreClientProvider.Client> {
@@ -338,3 +309,58 @@ export abstract class CoreClientAware {
338309
);
339310
}
340311
}
312+
313+
class IndexUpdateRequiredBeforeInitError extends Error {
314+
constructor(causes: RpcStatus.AsObject[]) {
315+
super(`The index of the cores and libraries must be updated before initializing the core gRPC client.
316+
The following problems were detected during the gRPC client initialization:
317+
${causes
318+
.map(({ code, message }) => ` - code: ${code}, message: ${message}`)
319+
.join('\n')}
320+
`);
321+
Object.setPrototypeOf(this, IndexUpdateRequiredBeforeInitError.prototype);
322+
if (!causes.length) {
323+
throw new Error(`expected non-empty 'causes'`);
324+
}
325+
}
326+
}
327+
328+
function isIndexUpdateRequiredBeforeInit(
329+
status: RpcStatus[]
330+
): IndexUpdateRequiredBeforeInitError | undefined {
331+
const causes = status
332+
.filter((s) =>
333+
IndexUpdateRequiredBeforeInit.map((predicate) => predicate(s)).some(
334+
Boolean
335+
)
336+
)
337+
.map((s) => RpcStatus.toObject(false, s));
338+
return causes.length
339+
? new IndexUpdateRequiredBeforeInitError(causes)
340+
: undefined;
341+
}
342+
const IndexUpdateRequiredBeforeInit = [
343+
isPackageIndexMissingStatus,
344+
isDiscoveryNotFoundStatus,
345+
];
346+
function isPackageIndexMissingStatus(status: RpcStatus): boolean {
347+
const predicate = ({ message }: RpcStatus.AsObject) =>
348+
message.includes('loading json index file');
349+
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247
350+
return evaluate(status, predicate);
351+
}
352+
function isDiscoveryNotFoundStatus(status: RpcStatus): boolean {
353+
const predicate = ({ message }: RpcStatus.AsObject) =>
354+
message.includes('discovery') &&
355+
(message.includes('not found') || message.includes('not installed'));
356+
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L740
357+
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L744
358+
return evaluate(status, predicate);
359+
}
360+
function evaluate(
361+
subject: RpcStatus,
362+
predicate: (error: RpcStatus.AsObject) => boolean
363+
): boolean {
364+
const status = RpcStatus.toObject(false, subject);
365+
return predicate(status);
366+
}

arduino-ide-extension/src/node/core-service-impl.ts

+53-24
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import { firstToUpperCase, firstToLowerCase } from '../common/utils';
2525
import { Port } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
2626
import { nls } from '@theia/core';
2727
import { MonitorManager } from './monitor-manager';
28+
import { SimpleBuffer } from './utils/simple-buffer';
2829

30+
const FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS = 32;
2931
@injectable()
3032
export class CoreServiceImpl extends CoreClientAware implements CoreService {
3133
@inject(ResponseService)
@@ -73,18 +75,25 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
7375
this.mergeSourceOverrides(compileReq, options);
7476

7577
const result = client.compile(compileReq);
78+
79+
const compileBuffer = new SimpleBuffer(
80+
this.flushOutputPanelMessages.bind(this),
81+
FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS
82+
);
7683
try {
7784
await new Promise<void>((resolve, reject) => {
7885
result.on('data', (cr: CompileResponse) => {
79-
this.responseService.appendToOutput({
80-
chunk: Buffer.from(cr.getOutStream_asU8()).toString(),
81-
});
82-
this.responseService.appendToOutput({
83-
chunk: Buffer.from(cr.getErrStream_asU8()).toString(),
84-
});
86+
compileBuffer.addChunk(cr.getOutStream_asU8());
87+
compileBuffer.addChunk(cr.getErrStream_asU8());
88+
});
89+
result.on('error', (error) => {
90+
compileBuffer.clearFlushInterval();
91+
reject(error);
92+
});
93+
result.on('end', () => {
94+
compileBuffer.clearFlushInterval();
95+
resolve();
8596
});
86-
result.on('error', (error) => reject(error));
87-
result.on('end', () => resolve());
8897
});
8998
this.responseService.appendToOutput({
9099
chunk: '\n--------------------------\nCompilation complete.\n',
@@ -176,17 +185,24 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
176185

177186
const result = responseHandler(client, req);
178187

188+
const uploadBuffer = new SimpleBuffer(
189+
this.flushOutputPanelMessages.bind(this),
190+
FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS
191+
);
192+
179193
await new Promise<void>((resolve, reject) => {
180194
result.on('data', (resp: UploadResponse) => {
181-
this.responseService.appendToOutput({
182-
chunk: Buffer.from(resp.getOutStream_asU8()).toString(),
183-
});
184-
this.responseService.appendToOutput({
185-
chunk: Buffer.from(resp.getErrStream_asU8()).toString(),
186-
});
195+
uploadBuffer.addChunk(resp.getOutStream_asU8());
196+
uploadBuffer.addChunk(resp.getErrStream_asU8());
197+
});
198+
result.on('error', (error) => {
199+
uploadBuffer.clearFlushInterval();
200+
reject(error);
201+
});
202+
result.on('end', () => {
203+
uploadBuffer.clearFlushInterval();
204+
resolve();
187205
});
188-
result.on('error', (error) => reject(error));
189-
result.on('end', () => resolve());
190206
});
191207
this.responseService.appendToOutput({
192208
chunk:
@@ -240,18 +256,25 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
240256
burnReq.setVerify(options.verify);
241257
burnReq.setVerbose(options.verbose);
242258
const result = client.burnBootloader(burnReq);
259+
260+
const bootloaderBuffer = new SimpleBuffer(
261+
this.flushOutputPanelMessages.bind(this),
262+
FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS
263+
);
243264
try {
244265
await new Promise<void>((resolve, reject) => {
245266
result.on('data', (resp: BurnBootloaderResponse) => {
246-
this.responseService.appendToOutput({
247-
chunk: Buffer.from(resp.getOutStream_asU8()).toString(),
248-
});
249-
this.responseService.appendToOutput({
250-
chunk: Buffer.from(resp.getErrStream_asU8()).toString(),
251-
});
267+
bootloaderBuffer.addChunk(resp.getOutStream_asU8());
268+
bootloaderBuffer.addChunk(resp.getErrStream_asU8());
269+
});
270+
result.on('error', (error) => {
271+
bootloaderBuffer.clearFlushInterval();
272+
reject(error);
273+
});
274+
result.on('end', () => {
275+
bootloaderBuffer.clearFlushInterval();
276+
resolve();
252277
});
253-
result.on('error', (error) => reject(error));
254-
result.on('end', () => resolve());
255278
});
256279
} catch (e) {
257280
const errorMessage = nls.localize(
@@ -283,4 +306,10 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
283306
}
284307
}
285308
}
309+
310+
private flushOutputPanelMessages(chunk: string): void {
311+
this.responseService.appendToOutput({
312+
chunk,
313+
});
314+
}
286315
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export class SimpleBuffer {
2+
private chunks: Uint8Array[] = [];
3+
4+
private flushInterval?: NodeJS.Timeout;
5+
6+
constructor(onFlush: (chunk: string) => void, flushTimeout: number) {
7+
this.flushInterval = setInterval(() => {
8+
if (this.chunks.length > 0) {
9+
const chunkString = Buffer.concat(this.chunks).toString();
10+
this.clearChunks();
11+
12+
onFlush(chunkString);
13+
}
14+
}, flushTimeout);
15+
}
16+
17+
public addChunk(chunk: Uint8Array): void {
18+
this.chunks.push(chunk);
19+
}
20+
21+
private clearChunks(): void {
22+
this.chunks = [];
23+
}
24+
25+
public clearFlushInterval(): void {
26+
this.clearChunks();
27+
28+
clearInterval(this.flushInterval);
29+
this.flushInterval = undefined;
30+
}
31+
}

0 commit comments

Comments
 (0)