|
1 | 1 | import { ClientDuplexStream } from '@grpc/grpc-js';
|
2 | 2 | import { Disposable, Emitter, ILogger } from '@theia/core';
|
3 | 3 | import { inject, named, postConstruct } from '@theia/core/shared/inversify';
|
| 4 | +import { diff, Operation } from 'just-diff'; |
4 | 5 | import { Board, Port, Status, Monitor } from '../common/protocol';
|
5 | 6 | import {
|
6 | 7 | EnumerateMonitorPortSettingsRequest,
|
@@ -80,6 +81,15 @@ export class MonitorService extends CoreClientAware implements Disposable {
|
80 | 81 | private readonly port: Port;
|
81 | 82 | private readonly monitorID: string;
|
82 | 83 |
|
| 84 | + /** |
| 85 | + * The lightweight representation of the port configuration currently in use for the running monitor. |
| 86 | + * IDE2 stores this object after starting the monitor. On every monitor settings change request, IDE2 compares |
| 87 | + * the current config with the new settings, and only sends the diff as the new config to overcome https://github.com/arduino/arduino-ide/issues/375. |
| 88 | + */ |
| 89 | + private currentPortConfigSnapshot: |
| 90 | + | MonitorPortConfiguration.AsObject |
| 91 | + | undefined; |
| 92 | + |
83 | 93 | constructor(
|
84 | 94 | @inject(MonitorServiceFactoryOptions) options: MonitorServiceFactoryOptions
|
85 | 95 | ) {
|
@@ -211,6 +221,14 @@ export class MonitorService extends CoreClientAware implements Disposable {
|
211 | 221 | monitorRequest
|
212 | 222 | );
|
213 | 223 | if (wroteToStreamSuccessfully) {
|
| 224 | + // Only store the config, if the monitor has successfully started. |
| 225 | + this.currentPortConfigSnapshot = MonitorPortConfiguration.toObject( |
| 226 | + false, |
| 227 | + config |
| 228 | + ); |
| 229 | + this.logger.info( |
| 230 | + `Using port configuration for ${this.port.protocol}:${this.port.address}: ${this.currentPortConfigSnapshot}` |
| 231 | + ); |
214 | 232 | this.startMessagesHandlers();
|
215 | 233 | this.logger.info(
|
216 | 234 | `started monitor to ${this.port?.address} using ${this.port?.protocol}`
|
@@ -518,16 +536,118 @@ export class MonitorService extends CoreClientAware implements Disposable {
|
518 | 536 | if (!this.duplex) {
|
519 | 537 | return Status.NOT_CONNECTED;
|
520 | 538 | }
|
| 539 | + |
| 540 | + const diffConfig = this.maybeUpdatePortConfigSnapshot(config); |
| 541 | + if (!diffConfig) { |
| 542 | + this.logger.info( |
| 543 | + `No port configuration changes have been detected. No need to send configure commands to the running monitor ${this.port.protocol}:${this.port.address}.` |
| 544 | + ); |
| 545 | + return Status.OK; |
| 546 | + } |
| 547 | + |
521 | 548 | const coreClient = await this.coreClient;
|
522 | 549 | const { instance } = coreClient;
|
523 | 550 |
|
| 551 | + this.logger.info( |
| 552 | + `Sending monitor request with new port configuration: ${JSON.stringify( |
| 553 | + MonitorPortConfiguration.toObject(false, diffConfig) |
| 554 | + )}` |
| 555 | + ); |
524 | 556 | const req = new MonitorRequest();
|
525 | 557 | req.setInstance(instance);
|
526 |
| - req.setPortConfiguration(config); |
| 558 | + req.setPortConfiguration(diffConfig); |
527 | 559 | this.duplex.write(req);
|
528 | 560 | return Status.OK;
|
529 | 561 | }
|
530 | 562 |
|
| 563 | + /** |
| 564 | + * Function to calculate a diff between the `otherPortConfig` argument and the `currentPortConfigSnapshot`. |
| 565 | + * |
| 566 | + * If the current config snapshot and the snapshot derived from `otherPortConfig` are the same, no snapshot update happens, |
| 567 | + * and the function returns with undefined. Otherwise, the current snapshot config value will be updated from the snapshot |
| 568 | + * derived from the `otherPortConfig` argument, and this function returns with a `MonitorPortConfiguration` instance |
| 569 | + * representing only the difference between the two snapshot configs to avoid sending unnecessary monitor to configure commands to the CLI. |
| 570 | + * See [#1703 (comment)](https://github.com/arduino/arduino-ide/pull/1703#issuecomment-1327913005) for more details. |
| 571 | + */ |
| 572 | + private maybeUpdatePortConfigSnapshot( |
| 573 | + otherPortConfig: MonitorPortConfiguration |
| 574 | + ): MonitorPortConfiguration | undefined { |
| 575 | + const otherPortConfigSnapshot = MonitorPortConfiguration.toObject( |
| 576 | + false, |
| 577 | + otherPortConfig |
| 578 | + ); |
| 579 | + if (!this.currentPortConfigSnapshot) { |
| 580 | + throw new Error( |
| 581 | + `The current port configuration object was undefined when tried to merge in ${JSON.stringify( |
| 582 | + otherPortConfigSnapshot |
| 583 | + )}.` |
| 584 | + ); |
| 585 | + } |
| 586 | + |
| 587 | + const snapshotDiff = diff( |
| 588 | + this.currentPortConfigSnapshot, |
| 589 | + otherPortConfigSnapshot |
| 590 | + ); |
| 591 | + if (!snapshotDiff.length) { |
| 592 | + return undefined; |
| 593 | + } |
| 594 | + |
| 595 | + const diffConfig = snapshotDiff.reduce((acc, curr) => { |
| 596 | + if (!this.isValidMonitorPortSettingChange(curr)) { |
| 597 | + throw new Error( |
| 598 | + `Expected only 'replace' operation: a 'value' change in the 'settingsList'. Calculated diff a ${JSON.stringify( |
| 599 | + snapshotDiff |
| 600 | + )} between ${JSON.stringify( |
| 601 | + this.currentPortConfigSnapshot |
| 602 | + )} and ${JSON.stringify( |
| 603 | + otherPortConfigSnapshot |
| 604 | + )} snapshots. Current JSON-patch entry was ${curr}.` |
| 605 | + ); |
| 606 | + } |
| 607 | + const { path, value } = curr; |
| 608 | + const [, index] = path; |
| 609 | + if (!this.currentPortConfigSnapshot?.settingsList) { |
| 610 | + throw new Error( |
| 611 | + `'settingsList' is missing from current port config snapshot: ${this.currentPortConfigSnapshot}` |
| 612 | + ); |
| 613 | + } |
| 614 | + const changedSetting = this.currentPortConfigSnapshot.settingsList[index]; |
| 615 | + const setting = new MonitorPortSetting(); |
| 616 | + setting.setValue(value); |
| 617 | + setting.setSettingId(changedSetting.settingId); |
| 618 | + acc.addSettings(setting); |
| 619 | + return acc; |
| 620 | + }, new MonitorPortConfiguration()); |
| 621 | + |
| 622 | + this.currentPortConfigSnapshot = otherPortConfigSnapshot; |
| 623 | + this.logger.info( |
| 624 | + `Updated the port configuration for ${this.port.protocol}:${ |
| 625 | + this.port.address |
| 626 | + }: ${JSON.stringify(this.currentPortConfigSnapshot)}` |
| 627 | + ); |
| 628 | + return diffConfig; |
| 629 | + } |
| 630 | + |
| 631 | + private isValidMonitorPortSettingChange(entry: { |
| 632 | + op: Operation; |
| 633 | + path: (string | number)[]; |
| 634 | + value: unknown; |
| 635 | + }): entry is { |
| 636 | + op: 'add'; |
| 637 | + path: ['settingsList', number, string]; |
| 638 | + value: string; |
| 639 | + } { |
| 640 | + const { op, path, value } = entry; |
| 641 | + return ( |
| 642 | + op === 'replace' && |
| 643 | + path.length === 3 && |
| 644 | + path[0] === 'settingsList' && |
| 645 | + typeof path[1] === 'number' && |
| 646 | + path[2] === 'value' && |
| 647 | + typeof value === 'string' |
| 648 | + ); |
| 649 | + } |
| 650 | + |
531 | 651 | /**
|
532 | 652 | * Starts the necessary handlers to send and receive
|
533 | 653 | * messages to and from the frontend and the running monitor
|
|
0 commit comments