Akos Kitta a36524e02a Update package index on 3rd party URLs change.
Closes #637
Closes #906

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-07-08 09:04:10 +02:00

208 lines
6.6 KiB
TypeScript

import { injectable, inject, postConstruct, named } from '@theia/core/shared/inversify';
import { ClientDuplexStream } from '@grpc/grpc-js';
import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone } from '@theia/core/lib/common/objects';
import { CoreClientAware, CoreClientProvider } from './core-client-provider';
import {
BoardListWatchRequest,
BoardListWatchResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
import {
Board,
Port,
NotificationServiceServer,
AvailablePorts,
AttachedBoardsChangeEvent,
} from '../common/protocol';
/**
* Singleton service for tracking the available ports and board and broadcasting the
* changes to all connected frontend instances. \
* Unlike other services, this is not connection scoped.
*/
@injectable()
export class BoardDiscovery extends CoreClientAware {
@inject(ILogger)
@named('discovery')
protected discoveryLogger: ILogger;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
// Used to know if the board watch process is already running to avoid
// starting it multiple times
private watching: boolean;
protected boardWatchDuplex:
| ClientDuplexStream<BoardListWatchRequest, BoardListWatchResponse>
| undefined;
/**
* Keys are the `address` of the ports. \
* The `protocol` is ignored because the board detach event does not carry the protocol information,
* just the address.
* ```json
* {
* "type": "remove",
* "address": "/dev/cu.usbmodem14101"
* }
* ```
*/
protected _state: AvailablePorts = {};
get state(): AvailablePorts {
return this._state;
}
@postConstruct()
protected async init(): Promise<void> {
this.coreClient.then((client) => this.startBoardListWatch(client));
}
stopBoardListWatch(coreClient: CoreClientProvider.Client): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.boardWatchDuplex) {
return resolve();
}
const { instance } = coreClient;
const req = new BoardListWatchRequest();
req.setInstance(instance);
try {
this.boardWatchDuplex.write(req.setInterrupt(true), resolve);
} catch (e) {
this.discoveryLogger.error(e);
resolve();
}
});
}
startBoardListWatch(coreClient: CoreClientProvider.Client): void {
if (this.watching) {
// We want to avoid starting the board list watch process multiple
// times to meet unforseen consequences
return;
}
this.watching = true;
const { client, instance } = coreClient;
const req = new BoardListWatchRequest();
req.setInstance(instance);
this.boardWatchDuplex = client.boardListWatch();
this.boardWatchDuplex.on('end', () => {
this.watching = false;
console.info('board watch ended');
});
this.boardWatchDuplex.on('close', () => {
this.watching = false;
console.info('board watch ended');
});
this.boardWatchDuplex.on('data', (resp: BoardListWatchResponse) => {
if (resp.getEventType() === 'quit') {
this.watching = false;
console.info('board watch ended');
return;
}
const detectedPort = resp.getPort();
if (detectedPort) {
let eventType: 'add' | 'remove' | 'unknown' = 'unknown';
if (resp.getEventType() === 'add') {
eventType = 'add';
} else if (resp.getEventType() === 'remove') {
eventType = 'remove';
} else {
eventType = 'unknown';
}
if (eventType === 'unknown') {
throw new Error(`Unexpected event type: '${resp.getEventType()}'`);
}
const oldState = deepClone(this._state);
const newState = deepClone(this._state);
const address = (detectedPort as any).getPort().getAddress();
const protocol = (detectedPort as any).getPort().getProtocol();
// Different discoveries can detect the same port with different
// protocols, so we consider the combination of address and protocol
// to be the id of a certain port to distinguish it from others.
// If we'd use only the address of a port to store it in a map
// we can have conflicts the same port is found with multiple
// protocols.
const portID = `${address}|${protocol}`;
const label = (detectedPort as any).getPort().getLabel();
const protocolLabel = (detectedPort as any).getPort().getProtocolLabel();
const port = {
id: portID,
address,
addressLabel: label,
protocol,
protocolLabel,
};
const boards: Board[] = [];
for (const item of detectedPort.getMatchingBoardsList()) {
boards.push({
fqbn: item.getFqbn(),
name: item.getName() || 'unknown',
port,
});
}
if (eventType === 'add') {
if (newState[portID]) {
const [, knownBoards] = newState[portID];
console.warn(
`Port '${Port.toString(port)}' was already available. Known boards before override: ${JSON.stringify(
knownBoards
)}`
);
}
newState[portID] = [port, boards];
} else if (eventType === 'remove') {
if (!newState[portID]) {
console.warn(`Port '${Port.toString(port)}' was not available. Skipping`);
return;
}
delete newState[portID];
}
const oldAvailablePorts = this.getAvailablePorts(oldState);
const oldAttachedBoards = this.getAttachedBoards(oldState);
const newAvailablePorts = this.getAvailablePorts(newState);
const newAttachedBoards = this.getAttachedBoards(newState);
const event: AttachedBoardsChangeEvent = {
oldState: {
ports: oldAvailablePorts,
boards: oldAttachedBoards,
},
newState: {
ports: newAvailablePorts,
boards: newAttachedBoards,
},
};
this._state = newState;
this.notificationService.notifyAttachedBoardsDidChange(event);
}
});
this.boardWatchDuplex.write(req);
}
getAttachedBoards(state: AvailablePorts = this.state): Board[] {
const attachedBoards: Board[] = [];
for (const portID of Object.keys(state)) {
const [, boards] = state[portID];
attachedBoards.push(...boards);
}
return attachedBoards;
}
getAvailablePorts(state: AvailablePorts = this.state): Port[] {
const availablePorts: Port[] = [];
for (const portID of Object.keys(state)) {
const [port] = state[portID];
availablePorts.push(port);
}
return availablePorts;
}
}