Use eslint&prettier for code linting&formatting

This commit is contained in:
Francesco Stasi
2021-06-16 15:08:48 +02:00
committed by Francesco Stasi
parent 2a3873a923
commit 0592199858
173 changed files with 8963 additions and 3841 deletions

View File

@@ -4,7 +4,10 @@ import { spawn, ChildProcess } from 'child_process';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { ILogger } from '@theia/core/lib/common/logger';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { environment } from '@theia/application-package/lib/environment';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
@@ -15,11 +18,12 @@ import { CLI_CONFIG } from './cli-config';
import { getExecPath, spawnCommand } from './exec-util';
@injectable()
export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContribution {
export class ArduinoDaemonImpl
implements ArduinoDaemon, BackendApplicationContribution
{
@inject(ILogger)
@named('daemon')
protected readonly logger: ILogger
protected readonly logger: ILogger;
@inject(EnvVariablesServer)
protected readonly envVariablesServer: EnvVariablesServer;
@@ -54,12 +58,20 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
this.onData(`Starting daemon from ${cliPath}...`);
const daemon = await this.spawnDaemonProcess();
// Watchdog process for terminating the daemon process when the backend app terminates.
spawn(process.execPath, [join(__dirname, 'daemon-watcher.js'), String(process.pid), String(daemon.pid)], {
env: environment.electron.runAsNodeEnv(),
detached: true,
stdio: 'ignore',
windowsHide: true
}).unref();
spawn(
process.execPath,
[
join(__dirname, 'daemon-watcher.js'),
String(process.pid),
String(daemon.pid),
],
{
env: environment.electron.runAsNodeEnv(),
detached: true,
stdio: 'ignore',
windowsHide: true,
}
).unref();
this.toDispose.pushAll([
Disposable.create(() => daemon.kill()),
@@ -73,7 +85,7 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
let i = 5; // TODO: make this better
while (i) {
this.onData(`Restarting daemon in ${i} seconds...`);
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
i--;
}
this.onData('Restarting daemon now...');
@@ -101,20 +113,29 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
if (this._execPath) {
return this._execPath;
}
this._execPath = await getExecPath('arduino-cli', this.onError.bind(this));
this._execPath = await getExecPath(
'arduino-cli',
this.onError.bind(this)
);
return this._execPath;
}
async getVersion(): Promise<Readonly<{ version: string, commit: string, status?: string }>> {
async getVersion(): Promise<
Readonly<{ version: string; commit: string; status?: string }>
> {
const execPath = await this.getExecPath();
const raw = await spawnCommand(`"${execPath}"`, ['version', '--format', 'json'], this.onError.bind(this));
const raw = await spawnCommand(
`"${execPath}"`,
['version', '--format', 'json'],
this.onError.bind(this)
);
try {
// The CLI `Info` object: https://github.com/arduino/arduino-cli/blob/17d24eb901b1fdaa5a4ec7da3417e9e460f84007/version/version.go#L31-L34
const { VersionString, Commit, Status } = JSON.parse(raw);
return {
version: VersionString,
commit: Commit,
status: Status
status: Status,
};
} catch {
return { version: raw, commit: raw };
@@ -124,20 +145,30 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
protected async getSpawnArgs(): Promise<string[]> {
const configDirUri = await this.envVariablesServer.getConfigDirUri();
const cliConfigPath = join(FileUri.fsPath(configDirUri), CLI_CONFIG);
return ['daemon', '--config-file', `"${cliConfigPath}"`, '-v', '--log-format', 'json'];
return [
'daemon',
'--config-file',
`"${cliConfigPath}"`,
'-v',
'--log-format',
'json',
];
}
protected async spawnDaemonProcess(): Promise<ChildProcess> {
const [cliPath, args] = await Promise.all([this.getExecPath(), this.getSpawnArgs()]);
const [cliPath, args] = await Promise.all([
this.getExecPath(),
this.getSpawnArgs(),
]);
const ready = new Deferred<ChildProcess>();
const options = { shell: true };
const daemon = spawn(`"${cliPath}"`, args, options);
// If the process exists right after the daemon gRPC server has started (due to an invalid port, unknown address, TCP port in use, etc.)
// If the process exists right after the daemon gRPC server has started (due to an invalid port, unknown address, TCP port in use, etc.)
// we have no idea about the root cause unless we sniff into the first data package and dispatch the logic on that. Note, we get a exit code 1.
let grpcServerIsReady = false;
daemon.stdout.on('data', data => {
daemon.stdout.on('data', (data) => {
const message = data.toString();
this.onData(message);
if (!grpcServerIsReady) {
@@ -145,7 +176,10 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
if (error) {
ready.reject(error);
}
for (const expected of ['Daemon is listening on TCP port', 'Daemon is now listening on 127.0.0.1']) {
for (const expected of [
'Daemon is listening on TCP port',
'Daemon is now listening on 127.0.0.1',
]) {
if (message.includes(expected)) {
grpcServerIsReady = true;
ready.resolve(daemon);
@@ -153,7 +187,7 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
}
}
});
daemon.stderr.on('data', data => {
daemon.stderr.on('data', (data) => {
const message = data.toString();
this.onData(data.toString());
const error = DaemonError.parse(message);
@@ -163,10 +197,16 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
if (code === 0 || signal === 'SIGINT' || signal === 'SIGKILL') {
this.onData('Daemon has stopped.');
} else {
this.onData(`Daemon exited with ${typeof code === 'undefined' ? `signal '${signal}'` : `exit code: ${code}`}.`);
this.onData(
`Daemon exited with ${
typeof code === 'undefined'
? `signal '${signal}'`
: `exit code: ${code}`
}.`
);
}
});
daemon.on('error', error => {
daemon.on('error', (error) => {
this.onError(error);
ready.reject(error);
});
@@ -198,20 +238,20 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
protected onError(error: any): void {
this.logger.error(error);
}
}
export class DaemonError extends Error {
constructor(message: string, public readonly code: number, public readonly details?: string) {
constructor(
message: string,
public readonly code: number,
public readonly details?: string
) {
super(message);
Object.setPrototypeOf(this, DaemonError.prototype);
}
}
export namespace DaemonError {
export const ADDRESS_IN_USE = 0;
export const UNKNOWN_ADDRESS = 2;
export const INVALID_PORT = 4;
@@ -220,22 +260,44 @@ export namespace DaemonError {
export function parse(log: string): DaemonError | undefined {
const raw = log.toLocaleLowerCase();
if (raw.includes('failed to listen')) {
if (raw.includes('address already in use') || (raw.includes('bind')) && raw.includes('only one usage of each socket address')) {
return new DaemonError('Failed to listen on TCP port. Address already in use.', DaemonError.ADDRESS_IN_USE);
if (
raw.includes('address already in use') ||
(raw.includes('bind') &&
raw.includes('only one usage of each socket address'))
) {
return new DaemonError(
'Failed to listen on TCP port. Address already in use.',
DaemonError.ADDRESS_IN_USE
);
}
if (raw.includes('is unknown name') || (raw.includes('tcp/') && (raw.includes('is an invalid port')))) {
return new DaemonError('Failed to listen on TCP port. Unknown address.', DaemonError.UNKNOWN_ADDRESS);
if (
raw.includes('is unknown name') ||
(raw.includes('tcp/') && raw.includes('is an invalid port'))
) {
return new DaemonError(
'Failed to listen on TCP port. Unknown address.',
DaemonError.UNKNOWN_ADDRESS
);
}
if (raw.includes('is an invalid port')) {
return new DaemonError('Failed to listen on TCP port. Invalid port.', DaemonError.INVALID_PORT);
return new DaemonError(
'Failed to listen on TCP port. Invalid port.',
DaemonError.INVALID_PORT
);
}
}
// Based on the CLI logging: `failed to serve`, and any other FATAL errors.
// https://github.com/arduino/arduino-cli/blob/11abbee8a9f027d087d4230f266a87217677d423/cli/daemon/daemon.go#L89-L94
if (raw.includes('failed to serve') && (raw.includes('"fatal"') || raw.includes('fata'))) {
return new DaemonError('Unexpected CLI start error.', DaemonError.UNKNOWN, log);
if (
raw.includes('failed to serve') &&
(raw.includes('"fatal"') || raw.includes('fata'))
) {
return new DaemonError(
'Unexpected CLI start error.',
DaemonError.UNKNOWN,
log
);
}
return undefined;
}
}

View File

@@ -1,9 +1,18 @@
import { ContainerModule } from 'inversify';
import { ArduinoDaemonImpl } from './arduino-daemon-impl';
import { ILogger } from '@theia/core/lib/common/logger';
import { BackendApplicationContribution, BackendApplication as TheiaBackendApplication } from '@theia/core/lib/node/backend-application';
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service';
import {
BackendApplicationContribution,
BackendApplication as TheiaBackendApplication,
} from '@theia/core/lib/node/backend-application';
import {
LibraryService,
LibraryServicePath,
} from '../common/protocol/library-service';
import {
BoardsService,
BoardsServicePath,
} from '../common/protocol/boards-service';
import { LibraryServiceImpl } from './library-service-server-impl';
import { BoardsServiceImpl } from './boards-service-impl';
import { CoreServiceImpl } from './core-service-impl';
@@ -14,24 +23,53 @@ import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core';
import { DefaultWorkspaceServer } from './theia/workspace/default-workspace-server';
import { WorkspaceServer as TheiaWorkspaceServer } from '@theia/workspace/lib/common';
import { SketchesServiceImpl } from './sketches-service-impl';
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
import { ConfigService, ConfigServicePath } from '../common/protocol/config-service';
import { ArduinoDaemon, ArduinoDaemonPath } from '../common/protocol/arduino-daemon';
import {
SketchesService,
SketchesServicePath,
} from '../common/protocol/sketches-service';
import {
ConfigService,
ConfigServicePath,
} from '../common/protocol/config-service';
import {
ArduinoDaemon,
ArduinoDaemonPath,
} from '../common/protocol/arduino-daemon';
import { MonitorServiceImpl } from './monitor/monitor-service-impl';
import { MonitorService, MonitorServicePath, MonitorServiceClient } from '../common/protocol/monitor-service';
import {
MonitorService,
MonitorServicePath,
MonitorServiceClient,
} from '../common/protocol/monitor-service';
import { MonitorClientProvider } from './monitor/monitor-client-provider';
import { ConfigServiceImpl } from './config-service-impl';
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
import { NodeFileSystemExt } from './node-filesystem-ext';
import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext';
import {
FileSystemExt,
FileSystemExtPath,
} from '../common/protocol/filesystem-ext';
import { ExamplesServiceImpl } from './examples-service-impl';
import { ExamplesService, ExamplesServicePath } from '../common/protocol/examples-service';
import { ExecutableService, ExecutableServicePath } from '../common/protocol/executable-service';
import {
ExamplesService,
ExamplesServicePath,
} from '../common/protocol/examples-service';
import {
ExecutableService,
ExecutableServicePath,
} from '../common/protocol/executable-service';
import { ExecutableServiceImpl } from './executable-service-impl';
import { ResponseServicePath, ResponseService } from '../common/protocol/response-service';
import {
ResponseServicePath,
ResponseService,
} from '../common/protocol/response-service';
import { NotificationServiceServerImpl } from './notification-service-server';
import { NotificationServiceServer, NotificationServiceClient, NotificationServicePath } from '../common/protocol';
import {
NotificationServiceServer,
NotificationServiceClient,
NotificationServicePath,
} from '../common/protocol';
import { BackendApplication } from './theia/core/backend-application';
import { BoardDiscovery } from './board-discovery';
import { DefaultGitInit } from './theia/git/git-init';
@@ -46,44 +84,78 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ConfigService).toService(ConfigServiceImpl);
// Note: The config service must start earlier than the daemon, hence the binding order of the BA contribution does matter.
bind(BackendApplicationContribution).toService(ConfigServiceImpl);
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(ConfigServicePath, () => context.container.get(ConfigService))).inSingletonScope();
bind(ConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(ConfigServicePath, () =>
context.container.get(ConfigService)
)
)
.inSingletonScope();
// Shared daemon
// Shared daemon
bind(ArduinoDaemonImpl).toSelf().inSingletonScope();
bind(ArduinoDaemon).toService(ArduinoDaemonImpl);
bind(BackendApplicationContribution).toService(ArduinoDaemonImpl);
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(ArduinoDaemonPath, () => context.container.get(ArduinoDaemon))).inSingletonScope();
bind(ConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(ArduinoDaemonPath, () =>
context.container.get(ArduinoDaemon)
)
)
.inSingletonScope();
// Examples service. One per backend, each connected FE gets a proxy.
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(ExamplesServiceImpl).toSelf().inSingletonScope();
bind(ExamplesService).toService(ExamplesServiceImpl);
bindBackendService(ExamplesServicePath, ExamplesService);
}));
bind(ConnectionContainerModule).toConstantValue(
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(ExamplesServiceImpl).toSelf().inSingletonScope();
bind(ExamplesService).toService(ExamplesServiceImpl);
bindBackendService(ExamplesServicePath, ExamplesService);
})
);
// Exposes the executable paths/URIs to the frontend
bind(ExecutableServiceImpl).toSelf().inSingletonScope();
bind(ExecutableService).toService(ExecutableServiceImpl);
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(ExecutableServicePath, () => context.container.get(ExecutableService))).inSingletonScope();
bind(ConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(ExecutableServicePath, () =>
context.container.get(ExecutableService)
)
)
.inSingletonScope();
// Library service. Singleton per backend, each connected FE gets its proxy.
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(LibraryServiceImpl).toSelf().inSingletonScope();
bind(LibraryService).toService(LibraryServiceImpl);
bindBackendService(LibraryServicePath, LibraryService);
}));
bind(ConnectionContainerModule).toConstantValue(
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(LibraryServiceImpl).toSelf().inSingletonScope();
bind(LibraryService).toService(LibraryServiceImpl);
bindBackendService(LibraryServicePath, LibraryService);
})
);
// Shared sketches service
bind(SketchesServiceImpl).toSelf().inSingletonScope();
bind(SketchesService).toService(SketchesServiceImpl);
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(SketchesServicePath, () => context.container.get(SketchesService))).inSingletonScope();
bind(ConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(SketchesServicePath, () =>
context.container.get(SketchesService)
)
)
.inSingletonScope();
// Boards service. One instance per connected frontend.
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(BoardsServiceImpl).toSelf().inSingletonScope();
bind(BoardsService).toService(BoardsServiceImpl);
bindBackendService(BoardsServicePath, BoardsService);
}));
bind(ConnectionContainerModule).toConstantValue(
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(BoardsServiceImpl).toSelf().inSingletonScope();
bind(BoardsService).toService(BoardsServiceImpl);
bindBackendService(BoardsServicePath, BoardsService);
})
);
// Shared Arduino core client provider service for the backend.
bind(CoreClientProvider).toSelf().inSingletonScope();
@@ -92,11 +164,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BoardDiscovery).toSelf().inSingletonScope();
// Core service -> `verify` and `upload`. Singleton per BE, each FE connection gets its proxy.
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(CoreServiceImpl).toSelf().inSingletonScope();
bind(CoreService).toService(CoreServiceImpl);
bindBackendService(CoreServicePath, CoreService);
}));
bind(ConnectionContainerModule).toConstantValue(
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(CoreServiceImpl).toSelf().inSingletonScope();
bind(CoreService).toService(CoreServiceImpl);
bindBackendService(CoreServicePath, CoreService);
})
);
// #region Theia customizations
@@ -109,64 +183,101 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// #endregion Theia customizations
// Monitor client provider per connected frontend.
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(MonitorClientProvider).toSelf().inSingletonScope();
bind(MonitorServiceImpl).toSelf().inSingletonScope();
bind(MonitorService).toService(MonitorServiceImpl);
bindBackendService<MonitorService, MonitorServiceClient>(MonitorServicePath, MonitorService, (service, client) => {
service.setClient(client);
client.onDidCloseConnection(() => service.dispose());
return service;
});
}));
bind(ConnectionContainerModule).toConstantValue(
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(MonitorClientProvider).toSelf().inSingletonScope();
bind(MonitorServiceImpl).toSelf().inSingletonScope();
bind(MonitorService).toService(MonitorServiceImpl);
bindBackendService<MonitorService, MonitorServiceClient>(
MonitorServicePath,
MonitorService,
(service, client) => {
service.setClient(client);
client.onDidCloseConnection(() => service.dispose());
return service;
}
);
})
);
// File-system extension for mapping paths to URIs
bind(NodeFileSystemExt).toSelf().inSingletonScope();
bind(FileSystemExt).toService(NodeFileSystemExt);
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(FileSystemExtPath, () => context.container.get(FileSystemExt))).inSingletonScope();
bind(ConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(FileSystemExtPath, () =>
context.container.get(FileSystemExt)
)
)
.inSingletonScope();
// Output service per connection.
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bindFrontendService }) => {
bindFrontendService(ResponseServicePath, ResponseService);
}));
bind(ConnectionContainerModule).toConstantValue(
ConnectionContainerModule.create(({ bindFrontendService }) => {
bindFrontendService(ResponseServicePath, ResponseService);
})
);
// Notify all connected frontend instances
bind(NotificationServiceServerImpl).toSelf().inSingletonScope();
bind(NotificationServiceServer).toService(NotificationServiceServerImpl);
bind(ConnectionHandler).toDynamicValue(context =>
new JsonRpcConnectionHandler<NotificationServiceClient>(NotificationServicePath, client => {
const server = context.container.get<NotificationServiceServer>(NotificationServiceServer);
server.setClient(client);
client.onDidCloseConnection(() => server.disposeClient(client));
return server;
})
).inSingletonScope();
bind(ConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler<NotificationServiceClient>(
NotificationServicePath,
(client) => {
const server =
context.container.get<NotificationServiceServer>(
NotificationServiceServer
);
server.setClient(client);
client.onDidCloseConnection(() =>
server.disposeClient(client)
);
return server;
}
)
)
.inSingletonScope();
// Logger for the Arduino daemon
bind(ILogger).toDynamicValue(ctx => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('daemon');
}).inSingletonScope().whenTargetNamed('daemon');
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('daemon');
})
.inSingletonScope()
.whenTargetNamed('daemon');
// Logger for the "serial discovery".
bind(ILogger).toDynamicValue(ctx => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('discovery');
}).inSingletonScope().whenTargetNamed('discovery');
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('discovery');
})
.inSingletonScope()
.whenTargetNamed('discovery');
// Logger for the CLI config service. From the CLI config (FS path aware), we make a URI-aware app config.
bind(ILogger).toDynamicValue(ctx => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('config');
}).inSingletonScope().whenTargetNamed('config');
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('config');
})
.inSingletonScope()
.whenTargetNamed('config');
// Logger for the monitor service.
bind(ILogger).toDynamicValue(ctx => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('monitor-service');
}).inSingletonScope().whenTargetNamed('monitor-service');
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('monitor-service');
})
.inSingletonScope()
.whenTargetNamed('monitor-service');
bind(DefaultGitInit).toSelf();
rebind(GitInit).toService(DefaultGitInit);
});

View File

@@ -3,8 +3,17 @@ import { ClientDuplexStream } from '@grpc/grpc-js';
import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone } from '@theia/core/lib/common/objects';
import { CoreClientAware } 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';
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
@@ -13,7 +22,6 @@ import { Board, Port, NotificationServiceServer, AvailablePorts, AttachedBoardsC
*/
@injectable()
export class BoardDiscovery extends CoreClientAware {
@inject(ILogger)
@named('discovery')
protected discoveryLogger: ILogger;
@@ -21,7 +29,9 @@ export class BoardDiscovery extends CoreClientAware {
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
protected boardWatchDuplex: ClientDuplexStream<BoardListWatchRequest, BoardListWatchResponse> | undefined;
protected boardWatchDuplex:
| ClientDuplexStream<BoardListWatchRequest, BoardListWatchResponse>
| undefined;
/**
* Keys are the `address` of the ports. \
@@ -49,7 +59,6 @@ export class BoardDiscovery extends CoreClientAware {
this.boardWatchDuplex.on('data', (resp: BoardListWatchResponse) => {
const detectedPort = resp.getPort();
if (detectedPort) {
let eventType: 'add' | 'remove' | 'unknown' = 'unknown';
if (resp.getEventType() === 'add') {
eventType = 'add';
@@ -60,30 +69,46 @@ export class BoardDiscovery extends CoreClientAware {
}
if (eventType === 'unknown') {
throw new Error(`Unexpected event type: '${resp.getEventType()}'`);
throw new Error(
`Unexpected event type: '${resp.getEventType()}'`
);
}
const oldState = deepClone(this._state);
const newState = deepClone(this._state);
const address = detectedPort.getAddress();
const protocol = Port.Protocol.toProtocol(detectedPort.getProtocol());
const protocol = Port.Protocol.toProtocol(
detectedPort.getProtocol()
);
// const label = detectedPort.getProtocolLabel();
const port = { address, protocol };
const boards: Board[] = [];
for (const item of detectedPort.getBoardsList()) {
boards.push({ fqbn: item.getFqbn(), name: item.getName() || 'unknown', port });
boards.push({
fqbn: item.getFqbn(),
name: item.getName() || 'unknown',
port,
});
}
if (eventType === 'add') {
if (newState[port.address] !== undefined) {
const [, knownBoards] = newState[port.address];
console.warn(`Port '${port.address}' was already available. Known boards before override: ${JSON.stringify(knownBoards)}`);
console.warn(
`Port '${
port.address
}' was already available. Known boards before override: ${JSON.stringify(
knownBoards
)}`
);
}
newState[port.address] = [port, boards];
} else if (eventType === 'remove') {
if (newState[port.address] === undefined) {
console.warn(`Port '${port.address}' was not available. Skipping`);
console.warn(
`Port '${port.address}' was not available. Skipping`
);
return;
}
delete newState[port.address];
@@ -96,12 +121,12 @@ export class BoardDiscovery extends CoreClientAware {
const event: AttachedBoardsChangeEvent = {
oldState: {
ports: oldAvailablePorts,
boards: oldAttachedBoards
boards: oldAttachedBoards,
},
newState: {
ports: newAvailablePorts,
boards: newAttachedBoards
}
boards: newAttachedBoards,
},
};
this._state = newState;
@@ -124,10 +149,9 @@ export class BoardDiscovery extends CoreClientAware {
const availablePorts: Port[] = [];
for (const address of Object.keys(state)) {
// tslint:disable-next-line: whitespace
const [port,] = state[address];
const [port] = state[address];
availablePorts.push(port);
}
return availablePorts;
}
}

View File

@@ -4,22 +4,46 @@ import { notEmpty } from '@theia/core/lib/common/objects';
import {
BoardsService,
Installable,
BoardsPackage, Board, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer, ResponseService, NotificationServiceServer, AvailablePorts, BoardWithPackage
BoardsPackage,
Board,
Port,
BoardDetails,
Tool,
ConfigOption,
ConfigValue,
Programmer,
ResponseService,
NotificationServiceServer,
AvailablePorts,
BoardWithPackage,
} from '../common/protocol';
import {
PlatformInstallRequest, PlatformListRequest, PlatformListResponse, PlatformSearchRequest,
PlatformSearchResponse, PlatformUninstallRequest
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 {
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 {
export class BoardsServiceImpl
extends CoreClientAware
implements BoardsService
{
@inject(ILogger)
protected logger: ILogger;
@@ -48,32 +72,43 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
return this.boardDiscovery.getAvailablePorts();
}
async getBoardDetails(options: { fqbn: string }): Promise<BoardDetails | undefined> {
async getBoardDetails(options: {
fqbn: string;
}): Promise<BoardDetails | undefined> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const { fqbn } = options;
const detailsReq = new BoardDetailsRequest();
detailsReq.setInstance(instance);
detailsReq.setFqbn(fqbn);
const detailsResp = await new Promise<BoardDetailsResponse | undefined>((resolve, reject) => client.boardDetails(detailsReq, (err, resp) => {
if (err) {
// Required cores are not installed manually: https://github.com/arduino/arduino-cli/issues/954
if ((err.message.indexOf('missing platform release') !== -1 && err.message.indexOf('referenced by board') !== -1)
// Platform is not installed.
|| err.message.indexOf('platform') !== -1 && err.message.indexOf('not installed') !== -1) {
resolve(undefined);
return;
}
// It's a hack to handle https://github.com/arduino/arduino-cli/issues/1262 gracefully.
if (err.message.indexOf('unknown package') !== -1) {
resolve(undefined);
return;
}
reject(err);
return;
}
resolve(resp);
}));
const detailsResp = await new Promise<BoardDetailsResponse | undefined>(
(resolve, reject) =>
client.boardDetails(detailsReq, (err, resp) => {
if (err) {
// Required cores are not installed manually: https://github.com/arduino/arduino-cli/issues/954
if (
(err.message.indexOf('missing platform release') !==
-1 &&
err.message.indexOf('referenced by board') !==
-1) ||
// Platform is not installed.
(err.message.indexOf('platform') !== -1 &&
err.message.indexOf('not installed') !== -1)
) {
resolve(undefined);
return;
}
// It's a hack to handle https://github.com/arduino/arduino-cli/issues/1262 gracefully.
if (err.message.indexOf('unknown package') !== -1) {
resolve(undefined);
return;
}
reject(err);
return;
}
resolve(resp);
})
);
if (!detailsResp) {
return undefined;
@@ -81,42 +116,64 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
const debuggingSupported = detailsResp.getDebuggingSupported();
const requiredTools = detailsResp.getToolsDependenciesList().map(t => <Tool>{
name: t.getName(),
packager: t.getPackager(),
version: t.getVersion()
});
const requiredTools = detailsResp.getToolsDependenciesList().map(
(t) =>
<Tool>{
name: t.getName(),
packager: t.getPackager(),
version: t.getVersion(),
}
);
const configOptions = detailsResp.getConfigOptionsList().map(c => <ConfigOption>{
label: c.getOptionLabel(),
option: c.getOption(),
values: c.getValuesList().map(v => <ConfigValue>{
value: v.getValue(),
label: v.getValueLabel(),
selected: v.getSelected()
})
});
const configOptions = detailsResp.getConfigOptionsList().map(
(c) =>
<ConfigOption>{
label: c.getOptionLabel(),
option: c.getOption(),
values: c.getValuesList().map(
(v) =>
<ConfigValue>{
value: v.getValue(),
label: v.getValueLabel(),
selected: v.getSelected(),
}
),
}
);
const listReq = new ListProgrammersAvailableForUploadRequest();
listReq.setInstance(instance);
listReq.setFqbn(fqbn);
const listResp = await new Promise<ListProgrammersAvailableForUploadResponse>((resolve, reject) => client.listProgrammersAvailableForUpload(listReq, (err, resp) => {
if (err) {
reject(err);
return;
}
resolve(resp);
}));
const listResp =
await new Promise<ListProgrammersAvailableForUploadResponse>(
(resolve, reject) =>
client.listProgrammersAvailableForUpload(
listReq,
(err, resp) => {
if (err) {
reject(err);
return;
}
resolve(resp);
}
)
);
const programmers = listResp.getProgrammersList().map(p => <Programmer>{
id: p.getId(),
name: p.getName(),
platform: p.getPlatform()
});
const programmers = listResp.getProgrammersList().map(
(p) =>
<Programmer>{
id: p.getId(),
name: p.getName(),
platform: p.getPlatform(),
}
);
let VID = 'N/A';
let PID = 'N/A';
const usbId = detailsResp.getIdentificationPrefsList().map(item => item.getUsbId()).find(notEmpty);
const usbId = detailsResp
.getIdentificationPrefsList()
.map((item) => item.getUsbId())
.find(notEmpty);
if (usbId) {
VID = usbId.getVid();
PID = usbId.getPid();
@@ -129,11 +186,13 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
programmers,
debuggingSupported,
VID,
PID
PID,
};
}
async getBoardPackage(options: { id: string }): Promise<BoardsPackage | undefined> {
async getBoardPackage(options: {
id: string;
}): Promise<BoardsPackage | undefined> {
const { id: expectedId } = options;
if (!expectedId) {
return undefined;
@@ -142,41 +201,51 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
return packages.find(({ id }) => id === expectedId);
}
async getContainerBoardPackage(options: { fqbn: string }): Promise<BoardsPackage | undefined> {
async getContainerBoardPackage(options: {
fqbn: string;
}): Promise<BoardsPackage | undefined> {
const { fqbn: expectedFqbn } = options;
if (!expectedFqbn) {
return undefined;
}
const packages = await this.search({});
return packages.find(({ boards }) => boards.some(({ fqbn }) => fqbn === expectedFqbn));
return packages.find(({ boards }) =>
boards.some(({ fqbn }) => fqbn === expectedFqbn)
);
}
async searchBoards({ query }: { query?: string }): Promise<BoardWithPackage[]> {
async searchBoards({
query,
}: {
query?: string;
}): Promise<BoardWithPackage[]> {
const { instance, client } = await this.coreClient();
const req = new BoardSearchRequest();
req.setSearchArgs(query || '');
req.setInstance(instance);
const boards = await new Promise<BoardWithPackage[]>((resolve, reject) => {
client.boardSearch(req, (error, resp) => {
if (error) {
reject(error);
return;
}
const boards: Array<BoardWithPackage> = [];
for (const board of resp.getBoardsList()) {
const platform = board.getPlatform();
if (platform) {
boards.push({
name: board.getName(),
fqbn: board.getFqbn(),
packageId: platform.getId(),
packageName: platform.getName()
});
const boards = await new Promise<BoardWithPackage[]>(
(resolve, reject) => {
client.boardSearch(req, (error, resp) => {
if (error) {
reject(error);
return;
}
}
resolve(boards);
})
});
const boards: Array<BoardWithPackage> = [];
for (const board of resp.getBoardsList()) {
const platform = board.getPlatform();
if (platform) {
boards.push({
name: board.getName(),
fqbn: board.getFqbn(),
packageId: platform.getId(),
packageName: platform.getName(),
});
}
}
resolve(boards);
});
}
);
return boards;
}
@@ -186,20 +255,31 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
const installedPlatformsReq = new PlatformListRequest();
installedPlatformsReq.setInstance(instance);
const installedPlatformsResp = await new Promise<PlatformListResponse>((resolve, reject) =>
client.platformList(installedPlatformsReq, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp))
const installedPlatformsResp = await new Promise<PlatformListResponse>(
(resolve, reject) =>
client.platformList(installedPlatformsReq, (err, resp) =>
(!!err ? reject : resolve)(!!err ? err : resp)
)
);
const installedPlatforms = installedPlatformsResp.getInstalledPlatformsList();
const installedPlatforms =
installedPlatformsResp.getInstalledPlatformsList();
const req = new PlatformSearchRequest();
req.setSearchArgs(options.query || '');
req.setAllVersions(true);
req.setInstance(instance);
const resp = await new Promise<PlatformSearchResponse>((resolve, reject) => client.platformSearch(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)));
const resp = await new Promise<PlatformSearchResponse>(
(resolve, reject) =>
client.platformSearch(req, (err, resp) =>
(!!err ? reject : resolve)(!!err ? err : resp)
)
);
const packages = new Map<string, BoardsPackage>();
const toPackage = (platform: Platform) => {
let installedVersion: string | undefined;
const matchingPlatform = installedPlatforms.find(ip => ip.getId() === platform.getId());
const matchingPlatform = installedPlatforms.find(
(ip) => ip.getId() === platform.getId()
);
if (!!matchingPlatform) {
installedVersion = matchingPlatform.getInstalled();
}
@@ -208,15 +288,22 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
name: platform.getName(),
author: platform.getMaintainer(),
availableVersions: [platform.getLatest()],
description: platform.getBoardsList().map(b => b.getName()).join(', '),
description: platform
.getBoardsList()
.map((b) => b.getName())
.join(', '),
installable: true,
deprecated: platform.getDeprecated(),
summary: 'Boards included in this package:',
installedVersion,
boards: platform.getBoardsList().map(b => <Board>{ name: b.getName(), fqbn: b.getFqbn() }),
moreInfoLink: platform.getWebsite()
}
}
boards: platform
.getBoardsList()
.map(
(b) => <Board>{ name: b.getName(), fqbn: b.getFqbn() }
),
moreInfoLink: platform.getWebsite(),
};
};
// We must group the cores by ID, and sort platforms by, first the installed version, then version alphabetical order.
// Otherwise we lose the FQBN information.
@@ -229,18 +316,32 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
groupedById.set(id, [platform]);
}
}
const installedAwareVersionComparator = (left: Platform, right: Platform) => {
const installedAwareVersionComparator = (
left: Platform,
right: Platform
) => {
// XXX: we cannot rely on `platform.getInstalled()`, it is always an empty string.
const leftInstalled = !!installedPlatforms.find(ip => ip.getId() === left.getId() && ip.getInstalled() === left.getLatest());
const rightInstalled = !!installedPlatforms.find(ip => ip.getId() === right.getId() && ip.getInstalled() === right.getLatest());
const leftInstalled = !!installedPlatforms.find(
(ip) =>
ip.getId() === left.getId() &&
ip.getInstalled() === left.getLatest()
);
const rightInstalled = !!installedPlatforms.find(
(ip) =>
ip.getId() === right.getId() &&
ip.getInstalled() === right.getLatest()
);
if (leftInstalled && !rightInstalled) {
return -1;
}
if (!leftInstalled && rightInstalled) {
return 1;
}
return Installable.Version.COMPARATOR(left.getLatest(), right.getLatest()); // Higher version comes first.
}
return Installable.Version.COMPARATOR(
left.getLatest(),
right.getLatest()
); // Higher version comes first.
};
for (const id of groupedById.keys()) {
groupedById.get(id)!.sort(installedAwareVersionComparator);
}
@@ -251,7 +352,9 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
const pkg = packages.get(id);
if (pkg) {
pkg.availableVersions.push(platform.getLatest());
pkg.availableVersions.sort(Installable.Version.COMPARATOR).reverse();
pkg.availableVersions
.sort(Installable.Version.COMPARATOR)
.reverse();
} else {
packages.set(id, toPackage(platform));
}
@@ -261,9 +364,15 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
return [...packages.values()];
}
async install(options: { item: BoardsPackage, progressId?: string, 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 version = !!options.version
? options.version
: item.availableVersions[0];
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
@@ -277,23 +386,37 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
console.info('>>> Starting boards package installation...', item);
const resp = client.platformInstall(req);
resp.on('data', InstallWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService }));
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.responseService.appendToOutput({ chunk: `Failed to install platform: ${item.id}.\n` });
this.responseService.appendToOutput({ chunk: error.toString() });
resp.on('error', (error) => {
this.responseService.appendToOutput({
chunk: `Failed to install platform: ${item.id}.\n`,
});
this.responseService.appendToOutput({
chunk: error.toString(),
});
reject(error);
});
});
const items = await this.search({});
const updated = items.find(other => BoardsPackage.equals(other, item)) || item;
const updated =
items.find((other) => BoardsPackage.equals(other, item)) || item;
this.notificationService.notifyPlatformInstalled({ item: updated });
console.info('<<< Boards package installation done.', item);
}
async uninstall(options: { item: BoardsPackage, progressId?: string }): Promise<void> {
async uninstall(options: {
item: BoardsPackage;
progressId?: string;
}): Promise<void> {
const { item, progressId } = options;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
@@ -307,7 +430,13 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
console.info('>>> Starting boards package uninstallation...', item);
const resp = client.platformUninstall(req);
resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService }));
resp.on(
'data',
InstallWithProgress.createDataCallback({
progressId,
responseService: this.responseService,
})
);
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);
@@ -317,5 +446,4 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
this.notificationService.notifyPlatformUninstalled({ item });
console.info('<<< Boards package uninstallation done.', item);
}
}

View File

@@ -6,15 +6,22 @@ export interface BoardManager {
readonly additional_urls: Array<string>;
}
export namespace BoardManager {
export function sameAs(left: RecursivePartial<BoardManager> | undefined, right: RecursivePartial<BoardManager> | undefined): boolean {
export function sameAs(
left: RecursivePartial<BoardManager> | undefined,
right: RecursivePartial<BoardManager> | undefined
): boolean {
const leftOrDefault = left || {};
const rightOrDefault = right || {};
const leftUrls = Array.from(new Set(leftOrDefault.additional_urls || []));
const rightUrls = Array.from(new Set(rightOrDefault.additional_urls || []));
const leftUrls = Array.from(
new Set(leftOrDefault.additional_urls || [])
);
const rightUrls = Array.from(
new Set(rightOrDefault.additional_urls || [])
);
if (leftUrls.length !== rightUrls.length) {
return false;
}
return leftUrls.every(url => rightUrls.indexOf(url) !== -1);
return leftUrls.every((url) => rightUrls.indexOf(url) !== -1);
}
}
@@ -22,10 +29,15 @@ export interface Daemon {
readonly port: string | number;
}
export namespace Daemon {
export function is(daemon: RecursivePartial<Daemon> | undefined): daemon is Daemon {
export function is(
daemon: RecursivePartial<Daemon> | undefined
): daemon is Daemon {
return !!daemon && !!daemon.port;
}
export function sameAs(left: RecursivePartial<Daemon> | undefined, right: RecursivePartial<Daemon> | undefined): boolean {
export function sameAs(
left: RecursivePartial<Daemon> | undefined,
right: RecursivePartial<Daemon> | undefined
): boolean {
if (left === undefined) {
return right === undefined;
}
@@ -42,22 +54,31 @@ export interface Directories {
readonly user: string;
}
export namespace Directories {
export function is(directories: RecursivePartial<Directories> | undefined): directories is Directories {
return !!directories
&& !!directories.data
&& !!directories.downloads
&& !!directories.user;
export function is(
directories: RecursivePartial<Directories> | undefined
): directories is Directories {
return (
!!directories &&
!!directories.data &&
!!directories.downloads &&
!!directories.user
);
}
export function sameAs(left: RecursivePartial<Directories> | undefined, right: RecursivePartial<Directories> | undefined): boolean {
export function sameAs(
left: RecursivePartial<Directories> | undefined,
right: RecursivePartial<Directories> | undefined
): boolean {
if (left === undefined) {
return right === undefined;
}
if (right === undefined) {
return left === undefined;
}
return left.data === right.data
&& left.downloads === right.downloads
&& left.user === right.user;
return (
left.data === right.data &&
left.downloads === right.downloads &&
left.user === right.user
);
}
}
@@ -67,11 +88,20 @@ export interface Logging {
level: Logging.Level;
}
export namespace Logging {
export type Format = 'text' | 'json';
export type Level = 'trace' | 'debug' | 'info' | 'warning' | 'error' | 'fatal' | 'panic';
export type Level =
| 'trace'
| 'debug'
| 'info'
| 'warning'
| 'error'
| 'fatal'
| 'panic';
export function sameAs(left: RecursivePartial<Logging> | undefined, right: RecursivePartial<Logging> | undefined): boolean {
export function sameAs(
left: RecursivePartial<Logging> | undefined,
right: RecursivePartial<Logging> | undefined
): boolean {
if (left === undefined) {
return right === undefined;
}
@@ -89,7 +119,6 @@ export namespace Logging {
}
return true;
}
}
export interface Network {
@@ -110,15 +139,24 @@ export interface DefaultCliConfig extends CliConfig {
daemon: Daemon;
}
export namespace DefaultCliConfig {
export function is(config: RecursivePartial<DefaultCliConfig> | undefined): config is DefaultCliConfig {
return !!config
&& Directories.is(config.directories)
&& Daemon.is(config.daemon);
export function is(
config: RecursivePartial<DefaultCliConfig> | undefined
): config is DefaultCliConfig {
return (
!!config &&
Directories.is(config.directories) &&
Daemon.is(config.daemon)
);
}
export function sameAs(left: DefaultCliConfig, right: DefaultCliConfig): boolean {
return Directories.sameAs(left.directories, right.directories)
&& Daemon.sameAs(left.daemon, right.daemon)
&& BoardManager.sameAs(left.board_manager, right.board_manager)
&& Logging.sameAs(left.logging, right.logging);
export function sameAs(
left: DefaultCliConfig,
right: DefaultCliConfig
): boolean {
return (
Directories.sameAs(left.directories, right.directories) &&
Daemon.sameAs(left.daemon, right.daemon) &&
BoardManager.sameAs(left.board_manager, right.board_manager) &&
Logging.sameAs(left.logging, right.logging)
);
}
}

View File

@@ -11,9 +11,17 @@ import { ILogger } from '@theia/core/lib/common/logger';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import { ConfigService, Config, NotificationServiceServer, Network } from '../common/protocol';
import {
ConfigService,
Config,
NotificationServiceServer,
Network,
} from '../common/protocol';
import { spawnCommand } from './exec-util';
import { MergeRequest, WriteRequest } from './cli-protocol/cc/arduino/cli/settings/v1/settings_pb';
import {
MergeRequest,
WriteRequest,
} from './cli-protocol/cc/arduino/cli/settings/v1/settings_pb';
import { SettingsServiceClient } from './cli-protocol/cc/arduino/cli/settings/v1/settings_grpc_pb';
import * as serviceGrpcPb from './cli-protocol/cc/arduino/cli/settings/v1/settings_grpc_pb';
import { ArduinoDaemonImpl } from './arduino-daemon-impl';
@@ -25,8 +33,9 @@ import { deepClone } from '@theia/core';
const track = temp.track();
@injectable()
export class ConfigServiceImpl implements BackendApplicationContribution, ConfigService {
export class ConfigServiceImpl
implements BackendApplicationContribution, ConfigService
{
@inject(ILogger)
@named('config')
protected readonly logger: ILogger;
@@ -74,20 +83,26 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
if (Config.sameAs(this.config, config)) {
return;
}
let copyDefaultCliConfig: DefaultCliConfig | undefined = deepClone(this.cliConfig);
let copyDefaultCliConfig: DefaultCliConfig | undefined = deepClone(
this.cliConfig
);
if (!copyDefaultCliConfig) {
copyDefaultCliConfig = await this.getFallbackCliConfig();
}
const { additionalUrls, dataDirUri, downloadsDirUri, sketchDirUri, network } = config;
const {
additionalUrls,
dataDirUri,
downloadsDirUri,
sketchDirUri,
network,
} = config;
copyDefaultCliConfig.directories = {
data: FileUri.fsPath(dataDirUri),
downloads: FileUri.fsPath(downloadsDirUri),
user: FileUri.fsPath(sketchDirUri)
user: FileUri.fsPath(sketchDirUri),
};
copyDefaultCliConfig.board_manager = {
additional_urls: [
...additionalUrls
]
additional_urls: [...additionalUrls],
};
const proxy = Network.stringify(network);
copyDefaultCliConfig.network = { proxy };
@@ -108,46 +123,67 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
return this.configChangeEmitter.event;
}
async getVersion(): Promise<Readonly<{ version: string, commit: string, status?: string }>> {
async getVersion(): Promise<
Readonly<{ version: string; commit: string; status?: string }>
> {
return this.daemon.getVersion();
}
async isInDataDir(uri: string): Promise<boolean> {
return this.getConfiguration().then(({ dataDirUri }) => new URI(dataDirUri).isEqualOrParent(new URI(uri)));
return this.getConfiguration().then(({ dataDirUri }) =>
new URI(dataDirUri).isEqualOrParent(new URI(uri))
);
}
async isInSketchDir(uri: string): Promise<boolean> {
return this.getConfiguration().then(({ sketchDirUri }) => new URI(sketchDirUri).isEqualOrParent(new URI(uri)));
return this.getConfiguration().then(({ sketchDirUri }) =>
new URI(sketchDirUri).isEqualOrParent(new URI(uri))
);
}
protected async loadCliConfig(): Promise<DefaultCliConfig | undefined> {
const cliConfigFileUri = await this.getCliConfigFileUri();
const cliConfigPath = FileUri.fsPath(cliConfigFileUri);
try {
const content = await promisify(fs.readFile)(cliConfigPath, { encoding: 'utf8' });
const content = await promisify(fs.readFile)(cliConfigPath, {
encoding: 'utf8',
});
const model = yaml.safeLoad(content) || {};
// The CLI can run with partial (missing `port`, `directories`), the app cannot, we merge the default with the user's config.
const fallbackModel = await this.getFallbackCliConfig();
return deepmerge(fallbackModel, model) as DefaultCliConfig;
} catch (error) {
this.logger.error(`Error occurred when loading CLI config from ${cliConfigPath}.`, error);
this.logger.error(
`Error occurred when loading CLI config from ${cliConfigPath}.`,
error
);
}
return undefined;
}
protected async getFallbackCliConfig(): Promise<DefaultCliConfig> {
const cliPath = await this.daemon.getExecPath();
const throwawayDirPath = await new Promise<string>((resolve, reject) => {
track.mkdir({}, (err, dirPath) => {
if (err) {
reject(err);
return;
}
resolve(dirPath);
});
});
await spawnCommand(`"${cliPath}"`, ['config', 'init', '--dest-dir', `"${throwawayDirPath}"`]);
const rawYaml = await promisify(fs.readFile)(path.join(throwawayDirPath, CLI_CONFIG), { encoding: 'utf-8' });
const throwawayDirPath = await new Promise<string>(
(resolve, reject) => {
track.mkdir({}, (err, dirPath) => {
if (err) {
reject(err);
return;
}
resolve(dirPath);
});
}
);
await spawnCommand(`"${cliPath}"`, [
'config',
'init',
'--dest-dir',
`"${throwawayDirPath}"`,
]);
const rawYaml = await promisify(fs.readFile)(
path.join(throwawayDirPath, CLI_CONFIG),
{ encoding: 'utf-8' }
);
const model = yaml.safeLoad(rawYaml.trim());
return model as DefaultCliConfig;
}
@@ -160,22 +196,36 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
await this.initCliConfigTo(path.dirname(cliConfigPath));
exists = await promisify(fs.exists)(cliConfigPath);
if (!exists) {
throw new Error(`Could not initialize the default CLI configuration file at ${cliConfigPath}.`);
throw new Error(
`Could not initialize the default CLI configuration file at ${cliConfigPath}.`
);
}
}
}
protected async initCliConfigTo(fsPathToDir: string): Promise<void> {
const cliPath = await this.daemon.getExecPath();
await spawnCommand(`"${cliPath}"`, ['config', 'init', '--dest-dir', `"${fsPathToDir}"`]);
await spawnCommand(`"${cliPath}"`, [
'config',
'init',
'--dest-dir',
`"${fsPathToDir}"`,
]);
}
protected async mapCliConfigToAppConfig(cliConfig: DefaultCliConfig): Promise<Config> {
protected async mapCliConfigToAppConfig(
cliConfig: DefaultCliConfig
): Promise<Config> {
const { directories } = cliConfig;
const { data, user, downloads } = directories;
const additionalUrls: Array<string> = [];
if (cliConfig.board_manager && cliConfig.board_manager.additional_urls) {
additionalUrls.push(...Array.from(new Set(cliConfig.board_manager.additional_urls)));
if (
cliConfig.board_manager &&
cliConfig.board_manager.additional_urls
) {
additionalUrls.push(
...Array.from(new Set(cliConfig.board_manager.additional_urls))
);
}
const network = Network.parse(cliConfig.network?.proxy);
return {
@@ -183,7 +233,7 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
sketchDirUri: FileUri.create(user).toString(),
downloadsDirUri: FileUri.create(downloads).toString(),
additionalUrls,
network
network,
};
}
@@ -196,14 +246,17 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
this.notificationService.notifyConfigChanged({ config: undefined });
}
protected async updateDaemon(port: string | number, config: DefaultCliConfig): Promise<void> {
protected async updateDaemon(
port: string | number,
config: DefaultCliConfig
): Promise<void> {
const client = this.createClient(port);
const req = new MergeRequest();
const json = JSON.stringify(config, null, 2);
req.setJsonData(json);
console.log(`Updating daemon with 'data': ${json}`);
return new Promise<void>((resolve, reject) => {
client.merge(req, error => {
client.merge(req, (error) => {
try {
if (error) {
reject(error);
@@ -224,7 +277,7 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
const cliConfigPath = FileUri.fsPath(cliConfigUri);
req.setFilePath(cliConfigPath);
return new Promise<void>((resolve, reject) => {
client.write(req, error => {
client.write(req, (error) => {
try {
if (error) {
reject(error);
@@ -240,9 +293,14 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
private createClient(port: string | number): SettingsServiceClient {
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
// @ts-ignore
const SettingsServiceClient = grpc.makeClientConstructor(serviceGrpcPb['cc.arduino.cli.settings.v1.SettingsService'], 'SettingsServiceService') as any;
return new SettingsServiceClient(`localhost:${port}`, grpc.credentials.createInsecure()) as SettingsServiceClient;
const SettingsServiceClient = grpc.makeClientConstructor(
// @ts-expect-error: ignore
serviceGrpcPb['cc.arduino.cli.settings.v1.SettingsService'],
'SettingsServiceService'
) as any;
return new SettingsServiceClient(
`localhost:${port}`,
grpc.credentials.createInsecure()
) as SettingsServiceClient;
}
}

View File

@@ -5,13 +5,19 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { GrpcClientProvider } from './grpc-client-provider';
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
import { InitRequest, InitResponse, UpdateIndexRequest, UpdateIndexResponse, UpdateLibrariesIndexRequest, UpdateLibrariesIndexResponse } from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
import {
InitRequest,
InitResponse,
UpdateIndexRequest,
UpdateIndexResponse,
UpdateLibrariesIndexRequest,
UpdateLibrariesIndexResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
import * as commandsGrpcPb from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { NotificationServiceServer } from '../common/protocol';
@injectable()
export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Client> {
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
@@ -25,7 +31,9 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
client.client.close();
}
protected async reconcileClient(port: string | number | undefined): Promise<void> {
protected async reconcileClient(
port: string | number | undefined
): Promise<void> {
if (port && port === this._port) {
// No need to create a new gRPC client, but we have to update the indexes.
if (this._client && !(this._client instanceof Error)) {
@@ -38,31 +46,45 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
}
}
protected async createClient(port: string | number): Promise<CoreClientProvider.Client> {
protected async createClient(
port: string | number
): Promise<CoreClientProvider.Client> {
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
// @ts-ignore
const ArduinoCoreServiceClient = grpc.makeClientConstructor(commandsGrpcPb['cc.arduino.cli.commands.v1.ArduinoCoreService'], 'ArduinoCoreServiceService') as any;
const client = new ArduinoCoreServiceClient(`localhost:${port}`, grpc.credentials.createInsecure(), this.channelOptions) as ArduinoCoreServiceClient;
const ArduinoCoreServiceClient = grpc.makeClientConstructor(
// @ts-expect-error: ignore
commandsGrpcPb['cc.arduino.cli.commands.v1.ArduinoCoreService'],
'ArduinoCoreServiceService'
) as any;
const client = new ArduinoCoreServiceClient(
`localhost:${port}`,
grpc.credentials.createInsecure(),
this.channelOptions
) as ArduinoCoreServiceClient;
const initReq = new InitRequest();
initReq.setLibraryManagerOnly(false);
const initResp = await new Promise<InitResponse>((resolve, reject) => {
let resp: InitResponse | undefined = undefined;
const stream = client.init(initReq);
stream.on('data', (data: InitResponse) => resp = data);
stream.on('data', (data: InitResponse) => (resp = data));
stream.on('end', () => resolve(resp!));
stream.on('error', err => reject(err));
stream.on('error', (err) => reject(err));
});
const instance = initResp.getInstance();
if (!instance) {
throw new Error('Could not retrieve instance from the initialize response.');
throw new Error(
'Could not retrieve instance from the initialize response.'
);
}
await this.updateIndexes({ instance, client });
return { instance, client };
}
protected async updateIndexes({ client, instance }: CoreClientProvider.Client): Promise<void> {
protected async updateIndexes({
client,
instance,
}: CoreClientProvider.Client): Promise<void> {
// in a separate promise, try and update the index
let indexUpdateSucceeded = true;
for (let i = 0; i < 10; i++) {
@@ -75,7 +97,9 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
}
}
if (!indexUpdateSucceeded) {
console.error('Could not update the index. Please restart to try again.');
console.error(
'Could not update the index. Please restart to try again.'
);
}
let libIndexUpdateSucceeded = true;
@@ -85,11 +109,16 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
libIndexUpdateSucceeded = true;
break;
} catch (e) {
console.error(`Error while updating library index in attempt ${i}.`, e);
console.error(
`Error while updating library index in attempt ${i}.`,
e
);
}
}
if (!libIndexUpdateSucceeded) {
console.error('Could not update the library index. Please restart to try again.');
console.error(
'Could not update the library index. Please restart to try again.'
);
}
if (indexUpdateSucceeded && libIndexUpdateSucceeded) {
@@ -97,7 +126,10 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
}
}
protected async updateLibraryIndex({ client, instance }: CoreClientProvider.Client): Promise<void> {
protected async updateLibraryIndex({
client,
instance,
}: CoreClientProvider.Client): Promise<void> {
const req = new UpdateLibrariesIndexRequest();
req.setInstance(instance);
const resp = client.updateLibrariesIndex(req);
@@ -116,7 +148,9 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
console.log(`Download of '${file}' completed.`);
}
} else {
console.log('The library index has been successfully updated.');
console.log(
'The library index has been successfully updated.'
);
}
file = undefined;
}
@@ -128,7 +162,10 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
});
}
protected async updateIndex({ client, instance }: CoreClientProvider.Client): Promise<void> {
protected async updateIndex({
client,
instance,
}: CoreClientProvider.Client): Promise<void> {
const updateReq = new UpdateIndexRequest();
updateReq.setInstance(instance);
const updateResp = client.updateIndex(updateReq);
@@ -158,7 +195,6 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
updateResp.on('end', resolve);
});
}
}
export namespace CoreClientProvider {
export interface Client {
@@ -169,34 +205,36 @@ export namespace CoreClientProvider {
@injectable()
export abstract class CoreClientAware {
@inject(CoreClientProvider)
protected readonly coreClientProvider: CoreClientProvider;
protected async coreClient(): Promise<CoreClientProvider.Client> {
const coreClient = await new Promise<CoreClientProvider.Client>(async (resolve, reject) => {
const handle = (c: CoreClientProvider.Client | Error) => {
if (c instanceof Error) {
reject(c);
} else {
resolve(c);
}
}
const client = await this.coreClientProvider.client();
if (client) {
handle(client);
return;
}
const toDispose = new DisposableCollection();
toDispose.push(this.coreClientProvider.onClientReady(async () => {
const coreClient = await new Promise<CoreClientProvider.Client>(
async (resolve, reject) => {
const handle = (c: CoreClientProvider.Client | Error) => {
if (c instanceof Error) {
reject(c);
} else {
resolve(c);
}
};
const client = await this.coreClientProvider.client();
if (client) {
handle(client);
return;
}
toDispose.dispose();
}));
});
const toDispose = new DisposableCollection();
toDispose.push(
this.coreClientProvider.onClientReady(async () => {
const client = await this.coreClientProvider.client();
if (client) {
handle(client);
}
toDispose.dispose();
})
);
}
);
return coreClient;
}
}

View File

@@ -5,9 +5,19 @@ 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 {
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 {
BurnBootloaderRequest,
BurnBootloaderResponse,
UploadRequest,
UploadResponse,
UploadUsingProgrammerRequest,
UploadUsingProgrammerResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
import { ResponseService } from '../common/protocol/response-service';
import { NotificationServiceServer } from '../common/protocol';
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
@@ -15,14 +25,18 @@ import { firstToUpperCase, firstToLowerCase } from '../common/utils';
@injectable()
export class CoreServiceImpl extends CoreClientAware implements CoreService {
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
async compile(options: CoreService.Compile.Options & { exportBinaries?: boolean, compilerWarnings?: CompilerWarnings }): Promise<void> {
async compile(
options: CoreService.Compile.Options & {
exportBinaries?: boolean;
compilerWarnings?: CompilerWarnings;
}
): Promise<void> {
const { sketchUri, fqbn, compilerWarnings } = options;
const sketchPath = FileUri.fsPath(sketchUri);
@@ -53,34 +67,59 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (cr: CompileResponse) => {
this.responseService.appendToOutput({ chunk: Buffer.from(cr.getOutStream_asU8()).toString() });
this.responseService.appendToOutput({ 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('error', (error) => reject(error));
result.on('end', () => resolve());
});
this.responseService.appendToOutput({ chunk: '\n--------------------------\nCompilation complete.\n' });
this.responseService.appendToOutput({
chunk: '\n--------------------------\nCompilation complete.\n',
});
} catch (e) {
this.responseService.appendToOutput({ chunk: `Compilation error: ${e}\n`, severity: 'error' });
this.responseService.appendToOutput({
chunk: `Compilation error: ${e}\n`,
severity: 'error',
});
throw e;
}
}
async upload(options: CoreService.Upload.Options): Promise<void> {
await this.doUpload(options, () => new UploadRequest(), (client, req) => client.upload(req));
await this.doUpload(
options,
() => new UploadRequest(),
(client, req) => client.upload(req)
);
}
async uploadUsingProgrammer(options: CoreService.Upload.Options): Promise<void> {
await this.doUpload(options, () => new UploadUsingProgrammerRequest(), (client, req) => client.uploadUsingProgrammer(req), 'upload using programmer');
async uploadUsingProgrammer(
options: CoreService.Upload.Options
): Promise<void> {
await this.doUpload(
options,
() => new UploadUsingProgrammerRequest(),
(client, req) => client.uploadUsingProgrammer(req),
'upload using programmer'
);
}
protected async doUpload(
options: CoreService.Upload.Options,
requestProvider: () => UploadRequest | UploadUsingProgrammerRequest,
// tslint:disable-next-line:max-line-length
responseHandler: (client: ArduinoCoreServiceClient, req: UploadRequest | UploadUsingProgrammerRequest) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
task: string = 'upload'): Promise<void> {
responseHandler: (
client: ArduinoCoreServiceClient,
req: UploadRequest | UploadUsingProgrammerRequest
) => ClientReadableStream<
UploadResponse | UploadUsingProgrammerResponse
>,
task = 'upload'
): Promise<void> {
await this.compile(Object.assign(options, { exportBinaries: false }));
const { sketchUri, fqbn, port, programmer } = options;
const sketchPath = FileUri.fsPath(sketchUri);
@@ -107,20 +146,34 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (resp: UploadResponse) => {
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() });
this.responseService.appendToOutput({ 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('error', (error) => reject(error));
result.on('end', () => resolve());
});
this.responseService.appendToOutput({ chunk: '\n--------------------------\n' + firstToLowerCase(task) + ' complete.\n' });
this.responseService.appendToOutput({
chunk:
'\n--------------------------\n' +
firstToLowerCase(task) +
' complete.\n',
});
} catch (e) {
this.responseService.appendToOutput({ chunk: `${firstToUpperCase(task)} error: ${e}\n`, severity: 'error' });
this.responseService.appendToOutput({
chunk: `${firstToUpperCase(task)} error: ${e}\n`,
severity: 'error',
});
throw e;
}
}
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
async burnBootloader(
options: CoreService.Bootloader.Options
): Promise<void> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const { fqbn, port, programmer } = options;
@@ -141,19 +194,29 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (resp: BurnBootloaderResponse) => {
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() });
this.responseService.appendToOutput({ 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('error', (error) => reject(error));
result.on('end', () => resolve());
});
} catch (e) {
this.responseService.appendToOutput({ 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;
}
}
private mergeSourceOverrides(req: { getSourceOverrideMap(): jspb.Map<string, string> }, options: CoreService.Compile.Options): void {
private mergeSourceOverrides(
req: { getSourceOverrideMap(): jspb.Map<string, string> },
options: CoreService.Compile.Options
): void {
const sketchPath = FileUri.fsPath(options.sketchUri);
for (const uri of Object.keys(options.sourceOverride)) {
const content = options.sourceOverride[uri];
@@ -163,5 +226,4 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
}
}
}
}

View File

@@ -7,7 +7,6 @@ export interface DaemonLog {
}
export namespace DaemonLog {
export interface Url {
readonly Scheme: string;
readonly Host: string;
@@ -15,19 +14,19 @@ export namespace DaemonLog {
}
export namespace Url {
export function is(arg: any | undefined): arg is Url {
return !!arg
&& typeof arg.Scheme === 'string'
&& typeof arg.Host === 'string'
&& typeof arg.Path === 'string';
return (
!!arg &&
typeof arg.Scheme === 'string' &&
typeof arg.Host === 'string' &&
typeof arg.Path === 'string'
);
}
export function toString(url: Url): string {
const { Scheme, Host, Path } = url;
return `${Scheme}://${Host}${Path}`;
}
}
export interface System {
@@ -37,7 +36,7 @@ export namespace DaemonLog {
export namespace System {
export function toString(system: System): string {
return `OS: ${system.os}`
return `OS: ${system.os}`;
}
}
@@ -47,36 +46,48 @@ export namespace DaemonLog {
}
export namespace Tool {
export function is(arg: any | undefined): arg is Tool {
return !!arg && typeof arg.version === 'string' && 'systems' in arg;
}
export function toString(tool: Tool): string {
const { version, systems } = tool;
return `Version: ${version}${!!systems ? ` Systems: [${tool.systems.map(System.toString).join(', ')}]` : ''}`;
return `Version: ${version}${
!!systems
? ` Systems: [${tool.systems
.map(System.toString)
.join(', ')}]`
: ''
}`;
}
}
export type Level = 'trace' | 'debug' | 'info' | 'warning' | 'error';
export function is(arg: any | undefined): arg is DaemonLog {
return !!arg
&& typeof arg.time === 'string'
&& typeof arg.level === 'string'
&& typeof arg.msg === 'string'
return (
!!arg &&
typeof arg.time === 'string' &&
typeof arg.level === 'string' &&
typeof arg.msg === 'string'
);
}
export function toLogLevel(log: DaemonLog): LogLevel {
const { level } = log;
switch (level) {
case 'trace': return LogLevel.TRACE;
case 'debug': return LogLevel.DEBUG;
case 'info': return LogLevel.INFO;
case 'warning': return LogLevel.WARN;
case 'error': return LogLevel.ERROR;
default: return LogLevel.INFO;
case 'trace':
return LogLevel.TRACE;
case 'debug':
return LogLevel.DEBUG;
case 'info':
return LogLevel.INFO;
case 'warning':
return LogLevel.WARN;
case 'error':
return LogLevel.ERROR;
default:
return LogLevel.INFO;
}
}
@@ -99,11 +110,13 @@ export namespace DaemonLog {
result.push(maybeDaemonLog);
continue;
}
} catch { /* NOOP */ }
} catch {
/* NOOP */
}
result.push({
time: new Date().toString(),
level: 'info',
msg: messages[i]
msg: messages[i],
});
}
return result;
@@ -111,13 +124,21 @@ export namespace DaemonLog {
export function toPrettyString(logMessages: string): string {
const parsed = parse(logMessages);
return parsed.map(log => toMessage(log)).join('\n') + '\n';
return parsed.map((log) => toMessage(log)).join('\n') + '\n';
}
function toMessage(log: DaemonLog, options: { omitLogLevel: boolean } = { omitLogLevel: false }): string {
const details = Object.keys(log).filter(key => key !== 'msg' && key !== 'level' && key !== 'time').map(key => toDetails(log, key)).join(', ');
const logLevel = options.omitLogLevel ? '' : `[${log.level.toUpperCase()}] `;
return `${logLevel}${log.msg}${!!details ? ` [${details}]` : ''}`
function toMessage(
log: DaemonLog,
options: { omitLogLevel: boolean } = { omitLogLevel: false }
): string {
const details = Object.keys(log)
.filter((key) => key !== 'msg' && key !== 'level' && key !== 'time')
.map((key) => toDetails(log, key))
.join(', ');
const logLevel = options.omitLogLevel
? ''
: `[${log.level.toUpperCase()}] `;
return `${logLevel}${log.msg}${!!details ? ` [${details}]` : ''}`;
}
function toDetails(log: DaemonLog, key: string): string {
@@ -131,5 +152,4 @@ export namespace DaemonLog {
}
return `${key.toLowerCase()}: ${value}`;
}
}

View File

@@ -1,6 +1,8 @@
import * as psTree from 'ps-tree';
const kill = require('tree-kill');
const [theiaPid, daemonPid] = process.argv.slice(2).map(id => Number.parseInt(id, 10));
const [theiaPid, daemonPid] = process.argv
.slice(2)
.map((id) => Number.parseInt(id, 10));
setInterval(() => {
try {

View File

@@ -7,12 +7,15 @@ import { notEmpty } from '@theia/core/lib/common/objects';
import { Sketch, SketchContainer } from '../common/protocol/sketches-service';
import { SketchesServiceImpl } from './sketches-service-impl';
import { ExamplesService } from '../common/protocol/examples-service';
import { LibraryLocation, LibraryPackage, LibraryService } from '../common/protocol';
import {
LibraryLocation,
LibraryPackage,
LibraryService,
} from '../common/protocol';
import { ConfigServiceImpl } from './config-service-impl';
@injectable()
export class ExamplesServiceImpl implements ExamplesService {
@inject(SketchesServiceImpl)
protected readonly sketchesService: SketchesServiceImpl;
@@ -35,22 +38,35 @@ export class ExamplesServiceImpl implements ExamplesService {
}
const exampleRootPath = join(__dirname, '..', '..', 'Examples');
const exampleNames = await promisify(fs.readdir)(exampleRootPath);
this._all = await Promise.all(exampleNames.map(name => join(exampleRootPath, name)).map(path => this.load(path)));
this._all = await Promise.all(
exampleNames
.map((name) => join(exampleRootPath, name))
.map((path) => this.load(path))
);
return this._all;
}
// TODO: decide whether it makes sense to cache them. Keys should be: `fqbn` + version of containing core/library.
async installed({ fqbn }: { fqbn?: string }): Promise<{ user: SketchContainer[], current: SketchContainer[], any: SketchContainer[] }> {
async installed({ fqbn }: { fqbn?: string }): Promise<{
user: SketchContainer[];
current: SketchContainer[];
any: SketchContainer[];
}> {
const user: SketchContainer[] = [];
const current: SketchContainer[] = [];
const any: SketchContainer[] = [];
const packages: LibraryPackage[] = await this.libraryService.list({ fqbn });
const packages: LibraryPackage[] = await this.libraryService.list({
fqbn,
});
for (const pkg of packages) {
const container = await this.tryGroupExamples(pkg);
const { location } = pkg;
if (location === LibraryLocation.USER) {
user.push(container);
} else if (location === LibraryLocation.PLATFORM_BUILTIN || LibraryLocation.REFERENCED_PLATFORM_BUILTIN) {
} else if (
location === LibraryLocation.PLATFORM_BUILTIN ||
LibraryLocation.REFERENCED_PLATFORM_BUILTIN
) {
current.push(container);
} else {
any.push(container);
@@ -64,25 +80,46 @@ export class ExamplesServiceImpl implements ExamplesService {
* folder hierarchy. This method tries to workaround it by falling back to the `installDirUri` and manually creating the
* location of the examples. Otherwise it creates the example container from the direct examples FS paths.
*/
protected async tryGroupExamples({ label, exampleUris, installDirUri }: LibraryPackage): Promise<SketchContainer> {
const paths = exampleUris.map(uri => FileUri.fsPath(uri));
protected async tryGroupExamples({
label,
exampleUris,
installDirUri,
}: LibraryPackage): Promise<SketchContainer> {
const paths = exampleUris.map((uri) => FileUri.fsPath(uri));
if (installDirUri) {
for (const example of ['example', 'Example', 'EXAMPLE', 'examples', 'Examples', 'EXAMPLES']) {
const examplesPath = join(FileUri.fsPath(installDirUri), example);
for (const example of [
'example',
'Example',
'EXAMPLE',
'examples',
'Examples',
'EXAMPLES',
]) {
const examplesPath = join(
FileUri.fsPath(installDirUri),
example
);
const exists = await promisify(fs.exists)(examplesPath);
const isDir = exists && (await promisify(fs.lstat)(examplesPath)).isDirectory();
const isDir =
exists &&
(await promisify(fs.lstat)(examplesPath)).isDirectory();
if (isDir) {
const fileNames = await promisify(fs.readdir)(examplesPath);
const children: SketchContainer[] = [];
const sketches: Sketch[] = [];
for (const fileName of fileNames) {
const subPath = join(examplesPath, fileName);
const subIsDir = (await promisify(fs.lstat)(subPath)).isDirectory();
const subIsDir = (
await promisify(fs.lstat)(subPath)
).isDirectory();
if (subIsDir) {
const sketch = await this.tryLoadSketch(subPath);
if (!sketch) {
const container = await this.load(subPath);
if (container.children.length || container.sketches.length) {
if (
container.children.length ||
container.sketches.length
) {
children.push(container);
}
} else {
@@ -93,22 +130,24 @@ export class ExamplesServiceImpl implements ExamplesService {
return {
label,
children,
sketches
sketches,
};
}
}
}
const sketches = await Promise.all(paths.map(path => this.tryLoadSketch(path)));
const sketches = await Promise.all(
paths.map((path) => this.tryLoadSketch(path))
);
return {
label,
children: [],
sketches: sketches.filter(notEmpty)
sketches: sketches.filter(notEmpty),
};
}
// Built-ins are included inside the IDE.
protected async load(path: string): Promise<SketchContainer> {
if (!await promisify(fs.exists)(path)) {
if (!(await promisify(fs.exists)(path))) {
throw new Error('Examples are not available');
}
const stat = await promisify(fs.stat)(path);
@@ -118,7 +157,7 @@ export class ExamplesServiceImpl implements ExamplesService {
const names = await promisify(fs.readdir)(path);
const sketches: Sketch[] = [];
const children: SketchContainer[] = [];
for (const p of names.map(name => join(path, name))) {
for (const p of names.map((name) => join(path, name))) {
const stat = await promisify(fs.stat)(p);
if (stat.isDirectory()) {
const sketch = await this.tryLoadSketch(p);
@@ -134,7 +173,7 @@ export class ExamplesServiceImpl implements ExamplesService {
return {
label,
children,
sketches
sketches,
};
}
@@ -149,11 +188,12 @@ export class ExamplesServiceImpl implements ExamplesService {
protected async tryLoadSketch(path: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchesService.loadSketch(FileUri.create(path).toString());
const sketch = await this.sketchesService.loadSketch(
FileUri.create(path).toString()
);
return sketch;
} catch {
return undefined;
}
}
}

View File

@@ -8,8 +8,8 @@ export async function getExecPath(
commandName: string,
onError: (error: Error) => void = (error) => console.log(error),
versionArg?: string | undefined,
inBinDir?: boolean): Promise<string> {
inBinDir?: boolean
): Promise<string> {
const execName = `${commandName}${os.platform() === 'win32' ? '.exe' : ''}`;
const relativePath = ['..', '..', 'build'];
if (inBinDir) {
@@ -20,13 +20,23 @@ export async function getExecPath(
return buildCommand;
}
const versionRegexp = /\d+\.\d+\.\d+/;
const buildVersion = await spawnCommand(`"${buildCommand}"`, [versionArg], onError);
const buildVersion = await spawnCommand(
`"${buildCommand}"`,
[versionArg],
onError
);
const buildShortVersion = (buildVersion.match(versionRegexp) || [])[0];
const pathCommand = await new Promise<string | undefined>(resolve => which(execName, (error, path) => resolve(error ? undefined : path)));
const pathCommand = await new Promise<string | undefined>((resolve) =>
which(execName, (error, path) => resolve(error ? undefined : path))
);
if (!pathCommand) {
return buildCommand;
}
const pathVersion = await spawnCommand(`"${pathCommand}"`, [versionArg], onError);
const pathVersion = await spawnCommand(
`"${pathCommand}"`,
[versionArg],
onError
);
const pathShortVersion = (pathVersion.match(versionRegexp) || [])[0];
if (semver.gt(pathShortVersion, buildShortVersion)) {
return pathCommand;
@@ -34,38 +44,52 @@ export async function getExecPath(
return buildCommand;
}
export function spawnCommand(command: string, args: string[], onError: (error: Error) => void = (error) => console.log(error)): Promise<string> {
export function spawnCommand(
command: string,
args: string[],
onError: (error: Error) => void = (error) => console.log(error)
): Promise<string> {
return new Promise<string>((resolve, reject) => {
const cp = spawn(command, args, { windowsHide: true, shell: true });
const outBuffers: Buffer[] = [];
const errBuffers: Buffer[] = [];
cp.stdout.on('data', (b: Buffer) => outBuffers.push(b));
cp.stderr.on('data', (b: Buffer) => errBuffers.push(b));
cp.on('error', error => {
cp.on('error', (error) => {
onError(error);
reject(error);
});
cp.on('exit', (code, signal) => {
if (code === 0) {
const result = Buffer.concat(outBuffers).toString('utf8').trim()
const result = Buffer.concat(outBuffers)
.toString('utf8')
.trim();
resolve(result);
return;
}
if (errBuffers.length > 0) {
const message = Buffer.concat(errBuffers).toString('utf8').trim();
const error = new Error(`Error executing ${command} ${args.join(' ')}: ${message}`);
onError(error)
const message = Buffer.concat(errBuffers)
.toString('utf8')
.trim();
const error = new Error(
`Error executing ${command} ${args.join(' ')}: ${message}`
);
onError(error);
reject(error);
return;
}
if (signal) {
const error = new Error(`Process exited with signal: ${signal}`);
const error = new Error(
`Process exited with signal: ${signal}`
);
onError(error);
reject(error);
return;
}
if (code) {
const error = new Error(`Process exited with exit code: ${code}`);
const error = new Error(
`Process exited with exit code: ${code}`
);
onError(error);
reject(error);
return;

View File

@@ -6,25 +6,27 @@ import { ExecutableService } from '../common/protocol/executable-service';
@injectable()
export class ExecutableServiceImpl implements ExecutableService {
@inject(ILogger)
protected logger: ILogger;
async list(): Promise<{ clangdUri: string, cliUri: string, lsUri: string }> {
async list(): Promise<{
clangdUri: string;
cliUri: string;
lsUri: string;
}> {
const [ls, clangd, cli] = await Promise.all([
getExecPath('arduino-language-server', this.onError.bind(this)),
getExecPath('clangd', this.onError.bind(this), undefined, true),
getExecPath('arduino-cli', this.onError.bind(this))
getExecPath('arduino-cli', this.onError.bind(this)),
]);
return {
clangdUri: FileUri.create(clangd).toString(),
cliUri: FileUri.create(cli).toString(),
lsUri: FileUri.create(ls).toString()
lsUri: FileUri.create(ls).toString(),
};
}
protected onError(error: Error): void {
this.logger.error(error);
}
}

View File

@@ -6,7 +6,6 @@ import { ArduinoDaemonImpl } from './arduino-daemon-impl';
@injectable()
export abstract class GrpcClientProvider<C> {
@inject(ILogger)
protected readonly logger: ILogger;
@@ -45,7 +44,9 @@ export abstract class GrpcClientProvider<C> {
}
}
protected async reconcileClient(port: string | number | undefined): Promise<void> {
protected async reconcileClient(
port: string | number | undefined
): Promise<void> {
if (this._port === port) {
return; // Nothing to do.
}
@@ -59,7 +60,7 @@ export abstract class GrpcClientProvider<C> {
const client = await this.createClient(this._port);
this._client = client;
} catch (error) {
this.logger.error('Could not create client for gRPC.', error)
this.logger.error('Could not create client for gRPC.', error);
this._client = error;
}
}
@@ -69,11 +70,10 @@ export abstract class GrpcClientProvider<C> {
protected abstract close(client: C): void;
protected get channelOptions(): object {
protected get channelOptions(): Record<string, unknown> {
return {
'grpc.max_send_message_length': 512 * 1024 * 1024,
'grpc.max_receive_message_length': 512 * 1024 * 1024
'grpc.max_receive_message_length': 512 * 1024 * 1024,
};
}
}

View File

@@ -1,5 +1,11 @@
import { ProgressMessage, ResponseService } from '../common/protocol/response-service';
import { DownloadProgress, TaskProgress } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
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;
@@ -7,7 +13,6 @@ export interface InstallResponse {
}
export namespace InstallWithProgress {
export interface Options {
/**
* _unknown_ progress if falsy.
@@ -16,23 +21,36 @@ export namespace InstallWithProgress {
readonly responseService: ResponseService;
}
export function createDataCallback({ responseService, progressId }: InstallWithProgress.Options): (response: InstallResponse) => void {
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 download = response.getProgress
? response.getProgress()
: undefined;
const task = response.getTaskProgress();
if (!download && !task) {
throw new Error("Implementation error. Neither 'download' nor 'task' is available.");
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.");
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.reportProgress({
progressId,
message,
work: { done: Number.NaN, total: Number.NaN },
});
}
responseService.appendToOutput({ chunk: `${message}\n` });
}
@@ -40,7 +58,10 @@ export namespace InstallWithProgress {
if (download.getFile() && !localFile) {
localFile = download.getFile();
}
if (download.getTotalSize() > 0 && Number.isNaN(localTotalSize)) {
if (
download.getTotalSize() > 0 &&
Number.isNaN(localTotalSize)
) {
localTotalSize = download.getTotalSize();
}
@@ -51,15 +72,29 @@ export namespace InstallWithProgress {
if (progressId && localFile) {
let work: ProgressMessage.Work | undefined = undefined;
if (download.getDownloaded() > 0 && !Number.isNaN(localTotalSize)) {
work = { total: localTotalSize, done: download.getDownloaded() };
if (
download.getDownloaded() > 0 &&
!Number.isNaN(localTotalSize)
) {
work = {
total: localTotalSize,
done: download.getDownloaded(),
};
}
responseService.reportProgress({ progressId, message: `Downloading ${localFile}`, work });
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 } });
responseService.reportProgress({
progressId,
message: '',
work: { done: Number.NaN, total: Number.NaN },
});
}
localFile = '';
localTotalSize = Number.NaN;
@@ -67,6 +102,4 @@ export namespace InstallWithProgress {
}
};
}
}

View File

@@ -1,10 +1,24 @@
import { injectable, inject } from 'inversify';
import { LibraryDependency, LibraryLocation, LibraryPackage, LibraryService } from '../common/protocol/library-service';
import {
LibraryDependency,
LibraryLocation,
LibraryPackage,
LibraryService,
} from '../common/protocol/library-service';
import { CoreClientAware } from './core-client-provider';
import {
InstalledLibrary, Library, LibraryInstallRequest, LibraryListRequest, LibraryListResponse, LibraryLocation as GrpcLibraryLocation, LibraryRelease,
LibraryResolveDependenciesRequest, LibraryUninstallRequest, ZipLibraryInstallRequest, LibrarySearchRequest,
LibrarySearchResponse
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';
@@ -13,8 +27,10 @@ import { ResponseService, NotificationServiceServer } from '../common/protocol';
import { InstallWithProgress } from './grpc-installable';
@injectable()
export class LibraryServiceImpl extends CoreClientAware implements LibraryService {
export class LibraryServiceImpl
extends CoreClientAware
implements LibraryService
{
@inject(ILogger)
protected logger: ILogger;
@@ -30,7 +46,12 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
const listReq = new LibraryListRequest();
listReq.setInstance(instance);
const installedLibsResp = await new Promise<LibraryListResponse>((resolve, reject) => client.libraryList(listReq, (err, resp) => !!err ? reject(err) : resolve(resp)));
const installedLibsResp = await new Promise<LibraryListResponse>(
(resolve, reject) =>
client.libraryList(listReq, (err, resp) =>
!!err ? reject(err) : resolve(resp)
)
);
const installedLibs = installedLibsResp.getInstalledLibrariesList();
const installedLibsIdx = new Map<string, InstalledLibrary>();
for (const installedLib of installedLibs) {
@@ -45,29 +66,48 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
const req = new LibrarySearchRequest();
req.setQuery(options.query || '');
req.setInstance(instance);
const resp = await new Promise<LibrarySearchResponse>((resolve, reject) => client.librarySearch(req, (err, resp) => !!err ? reject(err) : resolve(resp)));
const items = resp.getLibrariesList()
.filter(item => !!item.getLatest())
const resp = await new Promise<LibrarySearchResponse>(
(resolve, reject) =>
client.librarySearch(req, (err, resp) =>
!!err ? reject(err) : resolve(resp)
)
);
const items = resp
.getLibrariesList()
.filter((item) => !!item.getLatest())
.slice(0, 50)
.map(item => {
.map((item) => {
// TODO: This seems to contain only the latest item instead of all of the items.
const availableVersions = item.getReleasesMap().getEntryList().map(([key, _]) => key).sort(Installable.Version.COMPARATOR).reverse();
const availableVersions = item
.getReleasesMap()
.getEntryList()
.map(([key, _]) => key)
.sort(Installable.Version.COMPARATOR)
.reverse();
let installedVersion: string | undefined;
const installed = installedLibsIdx.get(item.getName());
if (installed) {
installedVersion = installed.getLibrary()!.getVersion();
}
return toLibrary({
name: item.getName(),
installable: true,
installedVersion,
}, item.getLatest()!, availableVersions)
return toLibrary(
{
name: item.getName(),
installable: true,
installedVersion,
},
item.getLatest()!,
availableVersions
);
});
return items;
}
async list({ fqbn }: { fqbn?: string | undefined }): Promise<LibraryPackage[]> {
async list({
fqbn,
}: {
fqbn?: string | undefined;
}): Promise<LibraryPackage[]> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new LibraryListRequest();
@@ -78,96 +118,145 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
req.setFqbn(fqbn);
}
const resp = await new Promise<LibraryListResponse | undefined>((resolve, reject) => {
client.libraryList(req, ((error, r) => {
if (error) {
const { message } = error;
// Required core dependency is missing.
// https://github.com/arduino/arduino-cli/issues/954
if (message.indexOf('missing platform release') !== -1 && message.indexOf('referenced by board') !== -1) {
resolve(undefined);
return;
}
// The core for the board is not installed, `lib list` cannot be filtered based on FQBN.
// https://github.com/arduino/arduino-cli/issues/955
if (message.indexOf('platform') !== -1 && message.indexOf('is not installed') !== -1) {
resolve(undefined);
return;
}
const resp = await new Promise<LibraryListResponse | undefined>(
(resolve, reject) => {
client.libraryList(req, (error, r) => {
if (error) {
const { message } = error;
// Required core dependency is missing.
// https://github.com/arduino/arduino-cli/issues/954
if (
message.indexOf('missing platform release') !==
-1 &&
message.indexOf('referenced by board') !== -1
) {
resolve(undefined);
return;
}
// The core for the board is not installed, `lib list` cannot be filtered based on FQBN.
// https://github.com/arduino/arduino-cli/issues/955
if (
message.indexOf('platform') !== -1 &&
message.indexOf('is not installed') !== -1
) {
resolve(undefined);
return;
}
// It's a hack to handle https://github.com/arduino/arduino-cli/issues/1262 gracefully.
if (message.indexOf('unknown package') !== -1) {
resolve(undefined);
// It's a hack to handle https://github.com/arduino/arduino-cli/issues/1262 gracefully.
if (message.indexOf('unknown package') !== -1) {
resolve(undefined);
return;
}
reject(error);
return;
}
reject(error);
return;
}
resolve(r);
}));
});
resolve(r);
});
}
);
if (!resp) {
return [];
}
return resp.getInstalledLibrariesList().map(item => {
const library = item.getLibrary();
if (!library) {
return undefined;
}
const installedVersion = library.getVersion();
return toLibrary({
name: library.getName(),
label: library.getRealName(),
installedVersion,
installable: true,
description: library.getSentence(),
summary: library.getParagraph(),
moreInfoLink: library.getWebsite(),
includes: library.getProvidesIncludesList(),
location: this.mapLocation(library.getLocation()),
installDirUri: FileUri.create(library.getInstallDir()).toString(),
exampleUris: library.getExamplesList().map(fsPath => FileUri.create(fsPath).toString())
}, library, [library.getVersion()]);
}).filter(notEmpty);
return resp
.getInstalledLibrariesList()
.map((item) => {
const library = item.getLibrary();
if (!library) {
return undefined;
}
const installedVersion = library.getVersion();
return toLibrary(
{
name: library.getName(),
label: library.getRealName(),
installedVersion,
installable: true,
description: library.getSentence(),
summary: library.getParagraph(),
moreInfoLink: library.getWebsite(),
includes: library.getProvidesIncludesList(),
location: this.mapLocation(library.getLocation()),
installDirUri: FileUri.create(
library.getInstallDir()
).toString(),
exampleUris: library
.getExamplesList()
.map((fsPath) => FileUri.create(fsPath).toString()),
},
library,
[library.getVersion()]
);
})
.filter(notEmpty);
}
private mapLocation(location: GrpcLibraryLocation): LibraryLocation {
switch (location) {
case GrpcLibraryLocation.LIBRARY_LOCATION_IDE_BUILTIN: return LibraryLocation.IDE_BUILTIN;
case GrpcLibraryLocation.LIBRARY_LOCATION_USER: return LibraryLocation.USER;
case GrpcLibraryLocation.LIBRARY_LOCATION_PLATFORM_BUILTIN: return LibraryLocation.PLATFORM_BUILTIN;
case GrpcLibraryLocation.LIBRARY_LOCATION_REFERENCED_PLATFORM_BUILTIN: return LibraryLocation.REFERENCED_PLATFORM_BUILTIN;
default: throw new Error(`Unexpected location ${location}.`);
case GrpcLibraryLocation.LIBRARY_LOCATION_IDE_BUILTIN:
return LibraryLocation.IDE_BUILTIN;
case GrpcLibraryLocation.LIBRARY_LOCATION_USER:
return LibraryLocation.USER;
case GrpcLibraryLocation.LIBRARY_LOCATION_PLATFORM_BUILTIN:
return LibraryLocation.PLATFORM_BUILTIN;
case GrpcLibraryLocation.LIBRARY_LOCATION_REFERENCED_PLATFORM_BUILTIN:
return LibraryLocation.REFERENCED_PLATFORM_BUILTIN;
default:
throw new Error(`Unexpected location ${location}.`);
}
}
async listDependencies({ item, version, filterSelf }: { item: LibraryPackage, version: Installable.Version, filterSelf?: boolean }): Promise<LibraryDependency[]> {
async listDependencies({
item,
version,
filterSelf,
}: {
item: LibraryPackage;
version: Installable.Version;
filterSelf?: boolean;
}): Promise<LibraryDependency[]> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new LibraryResolveDependenciesRequest();
req.setInstance(instance);
req.setName(item.name);
req.setVersion(version);
const dependencies = await new Promise<LibraryDependency[]>((resolve, reject) => {
client.libraryResolveDependencies(req, (error, resp) => {
if (error) {
reject(error);
return;
}
resolve(resp.getDependenciesList().map(dep => <LibraryDependency>{
name: dep.getName(),
installedVersion: dep.getVersionInstalled(),
requiredVersion: dep.getVersionRequired()
}));
})
});
return filterSelf ? dependencies.filter(({ name }) => name !== item.name) : dependencies;
const dependencies = await new Promise<LibraryDependency[]>(
(resolve, reject) => {
client.libraryResolveDependencies(req, (error, resp) => {
if (error) {
reject(error);
return;
}
resolve(
resp.getDependenciesList().map(
(dep) =>
<LibraryDependency>{
name: dep.getName(),
installedVersion: dep.getVersionInstalled(),
requiredVersion: dep.getVersionRequired(),
}
)
);
});
}
);
return filterSelf
? dependencies.filter(({ name }) => name !== item.name)
: dependencies;
}
async install(options: { item: LibraryPackage, progressId?: string, 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 version = !!options.version
? options.version
: item.availableVersions[0];
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
@@ -181,23 +270,44 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
console.info('>>> Starting library package installation...', item);
const resp = client.libraryInstall(req);
resp.on('data', InstallWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService }));
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.responseService.appendToOutput({ chunk: `Failed to install library: ${item.name}${version ? `:${version}` : ''}.\n` });
this.responseService.appendToOutput({ chunk: error.toString() });
resp.on('error', (error) => {
this.responseService.appendToOutput({
chunk: `Failed to install library: ${item.name}${
version ? `:${version}` : ''
}.\n`,
});
this.responseService.appendToOutput({
chunk: error.toString(),
});
reject(error);
});
});
const items = await this.search({});
const updated = items.find(other => LibraryPackage.equals(other, item)) || item;
const updated =
items.find((other) => LibraryPackage.equals(other, item)) || item;
this.notificationServer.notifyLibraryInstalled({ item: updated });
console.info('<<< Library package installation done.', item);
}
async installZip({ zipUri, progressId, overwrite }: { zipUri: string, progressId?: 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();
@@ -207,14 +317,23 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
req.setOverwrite(overwrite);
}
const resp = client.zipLibraryInstall(req);
resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService }));
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, progressId?: string }): Promise<void> {
async uninstall(options: {
item: LibraryPackage;
progressId?: string;
}): Promise<void> {
const { item, progressId } = options;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
@@ -226,7 +345,13 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
console.info('>>> Starting library package uninstallation...', item);
const resp = client.libraryUninstall(req);
resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService }));
resp.on(
'data',
InstallWithProgress.createDataCallback({
progressId,
responseService: this.responseService,
})
);
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);
@@ -240,10 +365,13 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
this.logger.info('>>> Disposing library service...');
this.logger.info('<<< Disposed library service.');
}
}
function toLibrary(pkg: Partial<LibraryPackage>, lib: LibraryRelease | Library, availableVersions: string[]): LibraryPackage {
function toLibrary(
pkg: Partial<LibraryPackage>,
lib: LibraryRelease | Library,
availableVersions: string[]
): LibraryPackage {
return {
name: '',
label: '',
@@ -258,6 +386,6 @@ function toLibrary(pkg: Partial<LibraryPackage>, lib: LibraryRelease | Library,
includes: lib.getProvidesIncludesList(),
description: lib.getSentence(),
moreInfoLink: lib.getWebsite(),
summary: lib.getParagraph()
}
summary: lib.getParagraph(),
};
}

View File

@@ -6,16 +6,21 @@ import { GrpcClientProvider } from '../grpc-client-provider';
@injectable()
export class MonitorClientProvider extends GrpcClientProvider<MonitorServiceClient> {
createClient(port: string | number): MonitorServiceClient {
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
// @ts-ignore
const MonitorServiceClient = grpc.makeClientConstructor(monitorGrpcPb['cc.arduino.cli.monitor.v1.MonitorService'], 'MonitorServiceService') as any;
return new MonitorServiceClient(`localhost:${port}`, grpc.credentials.createInsecure(), this.channelOptions);
const MonitorServiceClient = grpc.makeClientConstructor(
// @ts-expect-error: ignore
monitorGrpcPb['cc.arduino.cli.monitor.v1.MonitorService'],
'MonitorServiceService'
) as any;
return new MonitorServiceClient(
`localhost:${port}`,
grpc.credentials.createInsecure(),
this.channelOptions
);
}
close(client: MonitorServiceClient): void {
client.close();
}
}

View File

@@ -4,8 +4,18 @@ import { injectable, inject, named } from 'inversify';
import { Struct } from 'google-protobuf/google/protobuf/struct_pb';
import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { MonitorService, MonitorServiceClient, MonitorConfig, MonitorError, Status } from '../../common/protocol/monitor-service';
import { StreamingOpenRequest, StreamingOpenResponse, MonitorConfig as GrpcMonitorConfig } from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_pb';
import {
MonitorService,
MonitorServiceClient,
MonitorConfig,
MonitorError,
Status,
} from '../../common/protocol/monitor-service';
import {
StreamingOpenRequest,
StreamingOpenResponse,
MonitorConfig as GrpcMonitorConfig,
} from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_pb';
import { MonitorClientProvider } from './monitor-client-provider';
import { Board, Port } from '../../common/protocol/boards-service';
@@ -13,21 +23,33 @@ interface ErrorWithCode extends Error {
readonly code: number;
}
namespace ErrorWithCode {
export function toMonitorError(error: Error, config: MonitorConfig): MonitorError {
export function toMonitorError(
error: Error,
config: MonitorConfig
): MonitorError {
const { message } = error;
let code = undefined;
if (is(error)) {
// TODO: const `mapping`. Use regex for the `message`.
const mapping = new Map<string, number>();
mapping.set('1 CANCELLED: Cancelled on client', MonitorError.ErrorCodes.CLIENT_CANCEL);
mapping.set('2 UNKNOWN: device not configured', MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED);
mapping.set('2 UNKNOWN: error opening serial monitor: Serial port busy', MonitorError.ErrorCodes.DEVICE_BUSY);
mapping.set(
'1 CANCELLED: Cancelled on client',
MonitorError.ErrorCodes.CLIENT_CANCEL
);
mapping.set(
'2 UNKNOWN: device not configured',
MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED
);
mapping.set(
'2 UNKNOWN: error opening serial monitor: Serial port busy',
MonitorError.ErrorCodes.DEVICE_BUSY
);
code = mapping.get(message);
}
return {
message,
code,
config
config,
};
}
function is(error: Error & { code?: number }): error is ErrorWithCode {
@@ -37,7 +59,6 @@ namespace ErrorWithCode {
@injectable()
export class MonitorServiceImpl implements MonitorService {
@inject(ILogger)
@named('monitor-service')
protected readonly logger: ILogger;
@@ -46,7 +67,10 @@ export class MonitorServiceImpl implements MonitorService {
protected readonly monitorClientProvider: MonitorClientProvider;
protected client?: MonitorServiceClient;
protected connection?: { duplex: ClientDuplexStream<StreamingOpenRequest, StreamingOpenResponse>, config: MonitorConfig };
protected connection?: {
duplex: ClientDuplexStream<StreamingOpenRequest, StreamingOpenResponse>;
config: MonitorConfig;
};
protected messages: string[] = [];
protected onMessageDidReadEmitter = new Emitter<void>();
@@ -64,7 +88,11 @@ export class MonitorServiceImpl implements MonitorService {
}
async connect(config: MonitorConfig): Promise<Status> {
this.logger.info(`>>> Creating serial monitor connection for ${Board.toString(config.board)} on port ${Port.toString(config.port)}...`);
this.logger.info(
`>>> Creating serial monitor connection for ${Board.toString(
config.board
)} on port ${Port.toString(config.port)}...`
);
if (this.connection) {
return Status.ALREADY_CONNECTED;
}
@@ -78,25 +106,37 @@ export class MonitorServiceImpl implements MonitorService {
const duplex = client.streamingOpen();
this.connection = { duplex, config };
duplex.on('error', ((error: Error) => {
const monitorError = ErrorWithCode.toMonitorError(error, config);
this.disconnect(monitorError).then(() => {
if (this.client) {
this.client.notifyError(monitorError);
}
if (monitorError.code === undefined) {
// Log the original, unexpected error.
this.logger.error(error);
}
});
}).bind(this));
duplex.on(
'error',
((error: Error) => {
const monitorError = ErrorWithCode.toMonitorError(
error,
config
);
this.disconnect(monitorError).then(() => {
if (this.client) {
this.client.notifyError(monitorError);
}
if (monitorError.code === undefined) {
// Log the original, unexpected error.
this.logger.error(error);
}
});
}).bind(this)
);
duplex.on('data', ((resp: StreamingOpenResponse) => {
const raw = resp.getData();
const message = typeof raw === 'string' ? raw : new TextDecoder('utf8').decode(raw);
this.messages.push(message);
this.onMessageDidReadEmitter.fire();
}).bind(this));
duplex.on(
'data',
((resp: StreamingOpenResponse) => {
const raw = resp.getData();
const message =
typeof raw === 'string'
? raw
: new TextDecoder('utf8').decode(raw);
this.messages.push(message);
this.onMessageDidReadEmitter.fire();
}).bind(this)
);
const { type, port } = config;
const req = new StreamingOpenRequest();
@@ -104,14 +144,21 @@ export class MonitorServiceImpl implements MonitorService {
monitorConfig.setType(this.mapType(type));
monitorConfig.setTarget(port.address);
if (config.baudRate !== undefined) {
monitorConfig.setAdditionalConfig(Struct.fromJavaScript({ 'BaudRate': config.baudRate }));
monitorConfig.setAdditionalConfig(
Struct.fromJavaScript({ BaudRate: config.baudRate })
);
}
req.setConfig(monitorConfig);
return new Promise<Status>(resolve => {
return new Promise<Status>((resolve) => {
if (this.connection) {
this.connection.duplex.write(req, () => {
this.logger.info(`<<< Serial monitor connection created for ${Board.toString(config.board, { useFqbn: false })} on port ${Port.toString(config.port)}.`);
this.logger.info(
`<<< Serial monitor connection created for ${Board.toString(
config.board,
{ useFqbn: false }
)} on port ${Port.toString(config.port)}.`
);
resolve(Status.OK);
});
return;
@@ -122,7 +169,11 @@ export class MonitorServiceImpl implements MonitorService {
async disconnect(reason?: MonitorError): Promise<Status> {
try {
if (!this.connection && reason && reason.code === MonitorError.ErrorCodes.CLIENT_CANCEL) {
if (
!this.connection &&
reason &&
reason.code === MonitorError.ErrorCodes.CLIENT_CANCEL
) {
return Status.OK;
}
this.logger.info('>>> Disposing monitor connection...');
@@ -132,7 +183,12 @@ export class MonitorServiceImpl implements MonitorService {
}
const { duplex, config } = this.connection;
duplex.cancel();
this.logger.info(`<<< Disposed monitor connection for ${Board.toString(config.board, { useFqbn: false })} on port ${Port.toString(config.port)}.`);
this.logger.info(
`<<< Disposed monitor connection for ${Board.toString(
config.board,
{ useFqbn: false }
)} on port ${Port.toString(config.port)}.`
);
this.connection = undefined;
return Status.OK;
} finally {
@@ -146,7 +202,7 @@ export class MonitorServiceImpl implements MonitorService {
}
const req = new StreamingOpenRequest();
req.setData(new TextEncoder().encode(message));
return new Promise<Status>(resolve => {
return new Promise<Status>((resolve) => {
if (this.connection) {
this.connection.duplex.write(req, () => {
resolve(Status.OK);
@@ -162,7 +218,7 @@ export class MonitorServiceImpl implements MonitorService {
if (message) {
return { message };
}
return new Promise<{ message: string }>(resolve => {
return new Promise<{ message: string }>((resolve) => {
const toDispose = this.onMessageDidReadEmitter.event(() => {
toDispose.dispose();
resolve(this.request());
@@ -170,11 +226,14 @@ export class MonitorServiceImpl implements MonitorService {
});
}
protected mapType(type?: MonitorConfig.ConnectionType): GrpcMonitorConfig.TargetType {
protected mapType(
type?: MonitorConfig.ConnectionType
): GrpcMonitorConfig.TargetType {
switch (type) {
case MonitorConfig.ConnectionType.SERIAL: return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
default: return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
case MonitorConfig.ConnectionType.SERIAL:
return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
default:
return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
}
}
}

View File

@@ -4,9 +4,7 @@ import { FileSystemExt } from '../common/protocol/filesystem-ext';
@injectable()
export class NodeFileSystemExt implements FileSystemExt {
async getUri(fsPath: string): Promise<string> {
return FileUri.create(fsPath).toString()
return FileUri.create(fsPath).toString();
}
}

View File

@@ -1,49 +1,66 @@
import { injectable } from 'inversify';
import { NotificationServiceServer, NotificationServiceClient, AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config, Sketch } from '../common/protocol';
import {
NotificationServiceServer,
NotificationServiceClient,
AttachedBoardsChangeEvent,
BoardsPackage,
LibraryPackage,
Config,
Sketch,
} from '../common/protocol';
@injectable()
export class NotificationServiceServerImpl implements NotificationServiceServer {
export class NotificationServiceServerImpl
implements NotificationServiceServer
{
protected readonly clients: NotificationServiceClient[] = [];
notifyIndexUpdated(): void {
this.clients.forEach(client => client.notifyIndexUpdated());
this.clients.forEach((client) => client.notifyIndexUpdated());
}
notifyDaemonStarted(): void {
this.clients.forEach(client => client.notifyDaemonStarted());
this.clients.forEach((client) => client.notifyDaemonStarted());
}
notifyDaemonStopped(): void {
this.clients.forEach(client => client.notifyDaemonStopped());
this.clients.forEach((client) => client.notifyDaemonStopped());
}
notifyPlatformInstalled(event: { item: BoardsPackage }): void {
this.clients.forEach(client => client.notifyPlatformInstalled(event));
this.clients.forEach((client) => client.notifyPlatformInstalled(event));
}
notifyPlatformUninstalled(event: { item: BoardsPackage }): void {
this.clients.forEach(client => client.notifyPlatformUninstalled(event));
this.clients.forEach((client) =>
client.notifyPlatformUninstalled(event)
);
}
notifyLibraryInstalled(event: { item: LibraryPackage }): void {
this.clients.forEach(client => client.notifyLibraryInstalled(event));
this.clients.forEach((client) => client.notifyLibraryInstalled(event));
}
notifyLibraryUninstalled(event: { item: LibraryPackage }): void {
this.clients.forEach(client => client.notifyLibraryUninstalled(event));
this.clients.forEach((client) =>
client.notifyLibraryUninstalled(event)
);
}
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.clients.forEach(client => client.notifyAttachedBoardsChanged(event));
this.clients.forEach((client) =>
client.notifyAttachedBoardsChanged(event)
);
}
notifyConfigChanged(event: { config: Config | undefined }): void {
this.clients.forEach(client => client.notifyConfigChanged(event));
this.clients.forEach((client) => client.notifyConfigChanged(event));
}
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
this.clients.forEach(client => client.notifyRecentSketchesChanged(event));
this.clients.forEach((client) =>
client.notifyRecentSketchesChanged(event)
);
}
setClient(client: NotificationServiceClient): void {
@@ -53,7 +70,9 @@ export class NotificationServiceServerImpl implements NotificationServiceServer
disposeClient(client: NotificationServiceClient): void {
const index = this.clients.indexOf(client);
if (index === -1) {
console.warn('Could not dispose notification service client. It was not registered.');
console.warn(
'Could not dispose notification service client. It was not registered.'
);
return;
}
this.clients.splice(index, 1);
@@ -65,5 +84,4 @@ export class NotificationServiceServerImpl implements NotificationServiceServer
}
this.clients.length = 0;
}
}

View File

@@ -11,20 +11,29 @@ import URI from '@theia/core/lib/common/uri';
import { FileUri } from '@theia/core/lib/node';
import { isWindows } from '@theia/core/lib/common/os';
import { ConfigService } from '../common/protocol/config-service';
import { SketchesService, Sketch, SketchContainer } from '../common/protocol/sketches-service';
import {
SketchesService,
Sketch,
SketchContainer,
} from '../common/protocol/sketches-service';
import { firstToLowerCase } from '../common/utils';
import { NotificationServiceServerImpl } from './notification-service-server';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { CoreClientAware } from './core-client-provider';
import { ArchiveSketchRequest, LoadSketchRequest } from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
import {
ArchiveSketchRequest,
LoadSketchRequest,
} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/;
const prefix = '.arduinoIDE-unsaved';
@injectable()
export class SketchesServiceImpl extends CoreClientAware implements SketchesService {
export class SketchesServiceImpl
extends CoreClientAware
implements SketchesService
{
@inject(ConfigService)
protected readonly configService: ConfigService;
@@ -33,13 +42,20 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
async getSketches({ uri, exclude }: { uri?: string, exclude?: string[] }): Promise<SketchContainerWithDetails> {
async getSketches({
uri,
exclude,
}: {
uri?: string;
exclude?: string[];
}): Promise<SketchContainerWithDetails> {
const start = Date.now();
let sketchbookPath: undefined | string;
if (!uri) {
const { sketchDirUri } = await this.configService.getConfiguration();
const { sketchDirUri } =
await this.configService.getConfiguration();
sketchbookPath = FileUri.fsPath(sketchDirUri);
if (!await promisify(fs.exists)(sketchbookPath)) {
if (!(await promisify(fs.exists)(sketchbookPath))) {
await promisify(fs.mkdir)(sketchbookPath, { recursive: true });
}
} else {
@@ -48,9 +64,9 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
const container: SketchContainerWithDetails = {
label: uri ? path.basename(sketchbookPath) : 'Sketchbook',
sketches: [],
children: []
children: [],
};
if (!await promisify(fs.exists)(sketchbookPath)) {
if (!(await promisify(fs.exists)(sketchbookPath))) {
return container;
}
const stat = await promisify(fs.stat)(sketchbookPath);
@@ -58,12 +74,18 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
return container;
}
const recursivelyLoad = async (fsPath: string, containerToLoad: SketchContainerWithDetails) => {
const recursivelyLoad = async (
fsPath: string,
containerToLoad: SketchContainerWithDetails
) => {
const filenames = await promisify(fs.readdir)(fsPath);
for (const name of filenames) {
const childFsPath = path.join(fsPath, name);
let skip = false;
for (const pattern of exclude || ['**/libraries/**', '**/hardware/**']) {
for (const pattern of exclude || [
'**/libraries/**',
'**/hardware/**',
]) {
if (!skip && minimatch(childFsPath, pattern)) {
skip = true;
}
@@ -74,17 +96,19 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
try {
const stat = await promisify(fs.stat)(childFsPath);
if (stat.isDirectory()) {
const sketch = await this._isSketchFolder(FileUri.create(childFsPath).toString());
const sketch = await this._isSketchFolder(
FileUri.create(childFsPath).toString()
);
if (sketch) {
containerToLoad.sketches.push({
...sketch,
mtimeMs: stat.mtimeMs
mtimeMs: stat.mtimeMs,
});
} else {
const childContainer: SketchContainerWithDetails = {
label: name,
children: [],
sketches: []
sketches: [],
};
await recursivelyLoad(childFsPath, childContainer);
if (!SketchContainer.isEmpty(childContainer)) {
@@ -96,13 +120,19 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
console.warn(`Could not load sketch from ${childFsPath}.`);
}
}
containerToLoad.sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
containerToLoad.sketches.sort(
(left, right) => right.mtimeMs - left.mtimeMs
);
return containerToLoad;
}
};
await recursivelyLoad(sketchbookPath, container);
SketchContainer.prune(container);
console.debug(`Loading the sketches from ${sketchbookPath} took ${Date.now() - start} ms.`);
console.debug(
`Loading the sketches from ${sketchbookPath} took ${
Date.now() - start
} ms.`
);
return container;
}
@@ -111,38 +141,58 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
const req = new LoadSketchRequest();
req.setSketchPath(FileUri.fsPath(uri));
req.setInstance(instance);
const sketch = await new Promise<SketchWithDetails>((resolve, reject) => {
client.loadSketch(req, async (err, resp) => {
if (err) {
reject(err);
return;
}
const sketchFolderPath = resp.getLocationPath();
const { mtimeMs } = await promisify(fs.lstat)(sketchFolderPath);
resolve({
name: path.basename(sketchFolderPath),
uri: FileUri.create(sketchFolderPath).toString(),
mainFileUri: FileUri.create(resp.getMainFile()).toString(),
otherSketchFileUris: resp.getOtherSketchFilesList().map(p => FileUri.create(p).toString()),
additionalFileUris: resp.getAdditionalFilesList().map(p => FileUri.create(p).toString()),
rootFolderFileUris: resp.getRootFolderFilesList().map(p => FileUri.create(p).toString()),
mtimeMs
const sketch = await new Promise<SketchWithDetails>(
(resolve, reject) => {
client.loadSketch(req, async (err, resp) => {
if (err) {
reject(err);
return;
}
const sketchFolderPath = resp.getLocationPath();
const { mtimeMs } = await promisify(fs.lstat)(
sketchFolderPath
);
resolve({
name: path.basename(sketchFolderPath),
uri: FileUri.create(sketchFolderPath).toString(),
mainFileUri: FileUri.create(
resp.getMainFile()
).toString(),
otherSketchFileUris: resp
.getOtherSketchFilesList()
.map((p) => FileUri.create(p).toString()),
additionalFileUris: resp
.getAdditionalFilesList()
.map((p) => FileUri.create(p).toString()),
rootFolderFileUris: resp
.getRootFolderFilesList()
.map((p) => FileUri.create(p).toString()),
mtimeMs,
});
});
});
});
}
);
return sketch;
}
private get recentSketchesFsPath(): Promise<string> {
return this.envVariableServer.getConfigDirUri().then(uri => path.join(FileUri.fsPath(uri), 'recent-sketches.json'));
return this.envVariableServer
.getConfigDirUri()
.then((uri) =>
path.join(FileUri.fsPath(uri), 'recent-sketches.json')
);
}
private async loadRecentSketches(fsPath: string): Promise<Record<string, number>> {
private async loadRecentSketches(
fsPath: string
): Promise<Record<string, number>> {
let data: Record<string, number> = {};
try {
const raw = await promisify(fs.readFile)(fsPath, { encoding: 'utf8' });
const raw = await promisify(fs.readFile)(fsPath, {
encoding: 'utf8',
});
data = JSON.parse(raw);
} catch { }
} catch {}
return data;
}
@@ -178,24 +228,33 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
}
await promisify(fs.writeFile)(fsPath, JSON.stringify(data, null, 2));
this.recentlyOpenedSketches().then(sketches => this.notificationService.notifyRecentSketchesChanged({ sketches }));
this.recentlyOpenedSketches().then((sketches) =>
this.notificationService.notifyRecentSketchesChanged({ sketches })
);
}
async recentlyOpenedSketches(): Promise<Sketch[]> {
const configDirUri = await this.envVariableServer.getConfigDirUri();
const fsPath = path.join(FileUri.fsPath(configDirUri), 'recent-sketches.json');
const fsPath = path.join(
FileUri.fsPath(configDirUri),
'recent-sketches.json'
);
let data: Record<string, number> = {};
try {
const raw = await promisify(fs.readFile)(fsPath, { encoding: 'utf8' });
const raw = await promisify(fs.readFile)(fsPath, {
encoding: 'utf8',
});
data = JSON.parse(raw);
} catch { }
} catch {}
const sketches: SketchWithDetails[] = []
for (const uri of Object.keys(data).sort((left, right) => data[right] - data[left])) {
const sketches: SketchWithDetails[] = [];
for (const uri of Object.keys(data).sort(
(left, right) => data[right] - data[left]
)) {
try {
const sketch = await this.loadSketch(uri);
sketches.push(sketch);
} catch { }
} catch {}
}
return sketches;
@@ -210,15 +269,30 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
return;
}
resolve(dirPath);
})
});
});
const destinationUri = FileUri.create(path.join(parentPath, sketch.name)).toString();
const destinationUri = FileUri.create(
path.join(parentPath, sketch.name)
).toString();
const copiedSketchUri = await this.copy(sketch, { destinationUri });
return this.loadSketch(copiedSketchUri);
}
async createNewSketch(): Promise<Sketch> {
const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
const monthNames = [
'jan',
'feb',
'mar',
'apr',
'may',
'jun',
'jul',
'aug',
'sep',
'oct',
'nov',
'dec',
];
const today = new Date();
const parentPath = await new Promise<string>((resolve, reject) => {
temp.mkdir({ prefix }, (err, dirPath) => {
@@ -229,14 +303,20 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
resolve(dirPath);
});
});
const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`;
const sketchBaseName = `sketch_${
monthNames[today.getMonth()]
}${today.getDate()}`;
const config = await this.configService.getConfiguration();
const user = FileUri.fsPath(config.sketchDirUri);
let sketchName: string | undefined;
for (let i = 97; i < 97 + 26; i++) {
let sketchNameCandidate = `${sketchBaseName}${String.fromCharCode(i)}`;
const sketchNameCandidate = `${sketchBaseName}${String.fromCharCode(
i
)}`;
// Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder!
if (await promisify(fs.exists)(path.join(user, sketchNameCandidate))) {
if (
await promisify(fs.exists)(path.join(user, sketchNameCandidate))
) {
continue;
}
@@ -248,10 +328,12 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
throw new Error('Cannot create a unique sketch name');
}
const sketchDir = path.join(parentPath, sketchName)
const sketchDir = path.join(parentPath, sketchName);
const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
await promisify(fs.mkdir)(sketchDir, { recursive: true });
await promisify(fs.writeFile)(sketchFile, `void setup() {
await promisify(fs.writeFile)(
sketchFile,
`void setup() {
// put your setup code here, to run once:
}
@@ -260,7 +342,9 @@ void loop() {
// put your main code here, to run repeatedly:
}
`, { encoding: 'utf8' });
`,
{ encoding: 'utf8' }
);
return this.loadSketch(FileUri.create(sketchDir).toString());
}
@@ -284,21 +368,28 @@ void loop() {
return !!sketch;
}
private async _isSketchFolder(uri: string): Promise<SketchWithDetails | undefined> {
private async _isSketchFolder(
uri: string
): Promise<SketchWithDetails | undefined> {
const fsPath = FileUri.fsPath(uri);
let stat: fs.Stats | undefined;
try {
stat = await promisify(fs.lstat)(fsPath);
} catch { }
} catch {}
if (stat && stat.isDirectory()) {
const basename = path.basename(fsPath);
const files = await promisify(fs.readdir)(fsPath);
for (let i = 0; i < files.length; i++) {
if (files[i] === basename + '.ino' || files[i] === basename + '.pde') {
if (
files[i] === basename + '.ino' ||
files[i] === basename + '.pde'
) {
try {
const sketch = await this.loadSketch(FileUri.create(fsPath).toString());
const sketch = await this.loadSketch(
FileUri.create(fsPath).toString()
);
return sketch;
} catch { }
} catch {}
}
}
}
@@ -321,7 +412,10 @@ void loop() {
return sketchPath.indexOf(prefix) !== -1 && sketchPath.startsWith(temp);
}
async copy(sketch: Sketch, { destinationUri }: { destinationUri: string }): Promise<string> {
async copy(
sketch: Sketch,
{ destinationUri }: { destinationUri: string }
): Promise<string> {
const source = FileUri.fsPath(sketch.uri);
const exists = await promisify(fs.exists)(source);
if (!exists) {
@@ -335,26 +429,34 @@ void loop() {
const copy = async (sourcePath: string, destinationPath: string) => {
return new Promise<void>((resolve, reject) => {
ncp.ncp(sourcePath, destinationPath, async error => {
ncp.ncp(sourcePath, destinationPath, async (error) => {
if (error) {
reject(error);
return;
}
const newName = path.basename(destinationPath);
try {
const oldPath = path.join(destinationPath, new URI(sketch.mainFileUri).path.base);
const newPath = path.join(destinationPath, `${newName}.ino`);
const oldPath = path.join(
destinationPath,
new URI(sketch.mainFileUri).path.base
);
const newPath = path.join(
destinationPath,
`${newName}.ino`
);
if (oldPath !== newPath) {
await promisify(fs.rename)(oldPath, newPath);
}
await this.loadSketch(FileUri.create(destinationPath).toString()); // Sanity check.
await this.loadSketch(
FileUri.create(destinationPath).toString()
); // Sanity check.
resolve();
} catch (e) {
reject(e);
}
});
});
}
};
// https://github.com/arduino/arduino-ide/issues/65
// When copying `/path/to/sketchbook/sketch_A` to `/path/to/sketchbook/sketch_A/anything` on a non-POSIX filesystem,
// `ncp` makes a recursion and copies the folders over and over again. In such cases, we copy the source into a temp folder,
@@ -388,7 +490,7 @@ void loop() {
req.setSketchPath(FileUri.fsPath(sketch.uri));
req.setArchivePath(archivePath);
await new Promise<string>((resolve, reject) => {
client.archiveSketch(req, err => {
client.archiveSketch(req, (err) => {
if (err) {
reject(err);
return;
@@ -407,10 +509,12 @@ void loop() {
async getIdeTempFolderPath(sketch: Sketch): Promise<string> {
const sketchPath = FileUri.fsPath(sketch.uri);
await fs.promises.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible.
const suffix = crypto.createHash('md5').update(sketchPath).digest('hex');
const suffix = crypto
.createHash('md5')
.update(sketchPath)
.digest('hex');
return path.join(os.tmpdir(), `arduino-ide2-${suffix}`);
}
}
interface SketchWithDetails extends Sketch {

View File

@@ -1,19 +1,26 @@
import { inject, injectable, named } from 'inversify';
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { BackendApplication as TheiaBackendApplication, BackendApplicationContribution, BackendApplicationCliContribution } from '@theia/core/lib/node/backend-application';
import {
BackendApplication as TheiaBackendApplication,
BackendApplicationContribution,
BackendApplicationCliContribution,
} from '@theia/core/lib/node/backend-application';
@injectable()
export class BackendApplication extends TheiaBackendApplication {
constructor(
@inject(ContributionProvider) @named(BackendApplicationContribution) protected readonly contributionsProvider: ContributionProvider<BackendApplicationContribution>,
@inject(BackendApplicationCliContribution) protected readonly cliParams: BackendApplicationCliContribution
@inject(ContributionProvider)
@named(BackendApplicationContribution)
protected readonly contributionsProvider: ContributionProvider<BackendApplicationContribution>,
@inject(BackendApplicationCliContribution)
protected readonly cliParams: BackendApplicationCliContribution
) {
super(contributionsProvider, cliParams);
// Workaround for Electron not installing a handler to ignore SIGPIPE
// (https://github.com/electron/electron/issues/13254)
// From VS Code: https://github.com/microsoft/vscode/blob/d0c90c9f3ea8d34912194176241503a44b3abd80/src/bootstrap.js#L31-L37
process.on('SIGPIPE', () => console.error(new Error('Unexpected SIGPIPE signal.')));
process.on('SIGPIPE', () =>
console.error(new Error('Unexpected SIGPIPE signal.'))
);
}
}

View File

@@ -7,7 +7,12 @@ import { EnvVariablesServerImpl as TheiaEnvVariablesServerImpl } from '@theia/co
@injectable()
export class EnvVariablesServer extends TheiaEnvVariablesServerImpl {
protected readonly configDirUri = Promise.resolve(FileUri.create(join(homedir(), BackendApplicationConfigProvider.get().configDirName)).toString());
protected readonly configDirUri = Promise.resolve(
FileUri.create(
join(
homedir(),
BackendApplicationConfigProvider.get().configDirName
)
).toString()
);
}

View File

@@ -7,7 +7,6 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
@injectable()
export class DefaultGitInit implements GitInit {
protected readonly toDispose = new DisposableCollection();
async init(): Promise<void> {
@@ -16,19 +15,35 @@ export class DefaultGitInit implements GitInit {
const { execPath, path, version } = await findGit();
if (!!execPath && !!path && !!version) {
const dir = dirname(dirname(path));
const [execPathOk, pathOk, dirOk] = await Promise.all([pathExists(execPath), pathExists(path), pathExists(dir)]);
const [execPathOk, pathOk, dirOk] = await Promise.all([
pathExists(execPath),
pathExists(path),
pathExists(dir),
]);
if (execPathOk && pathOk && dirOk) {
if (typeof env.LOCAL_GIT_DIRECTORY !== 'undefined' && env.LOCAL_GIT_DIRECTORY !== dir) {
console.error(`Misconfigured env.LOCAL_GIT_DIRECTORY: ${env.LOCAL_GIT_DIRECTORY}. dir was: ${dir}`);
if (
typeof env.LOCAL_GIT_DIRECTORY !== 'undefined' &&
env.LOCAL_GIT_DIRECTORY !== dir
) {
console.error(
`Misconfigured env.LOCAL_GIT_DIRECTORY: ${env.LOCAL_GIT_DIRECTORY}. dir was: ${dir}`
);
return;
}
if (typeof env.GIT_EXEC_PATH !== 'undefined' && env.GIT_EXEC_PATH !== execPath) {
console.error(`Misconfigured env.GIT_EXEC_PATH: ${env.GIT_EXEC_PATH}. execPath was: ${execPath}`);
if (
typeof env.GIT_EXEC_PATH !== 'undefined' &&
env.GIT_EXEC_PATH !== execPath
) {
console.error(
`Misconfigured env.GIT_EXEC_PATH: ${env.GIT_EXEC_PATH}. execPath was: ${execPath}`
);
return;
}
process.env.LOCAL_GIT_DIRECTORY = dir;
process.env.GIT_EXEC_PATH = execPath;
console.info(`Using Git [${version}] from the PATH. (${path})`);
console.info(
`Using Git [${version}] from the PATH. (${path})`
);
return;
}
}
@@ -40,5 +55,4 @@ export class DefaultGitInit implements GitInit {
dispose(): void {
this.toDispose.dispose();
}
}

View File

@@ -5,7 +5,6 @@ import { ConfigService } from '../../../common/protocol/config-service';
@injectable()
export class DefaultWorkspaceServer extends TheiaDefaultWorkspaceServer {
@inject(ConfigService)
protected readonly configService: ConfigService;
@@ -17,9 +16,10 @@ export class DefaultWorkspaceServer extends TheiaDefaultWorkspaceServer {
const config = await this.configService.getConfiguration();
return config.sketchDirUri;
} catch (err) {
this.logger.error(`Failed to determine the sketch directory: ${err}`);
this.logger.error(
`Failed to determine the sketch directory: ${err}`
);
return super.getWorkspaceURIFromCli();
}
}
}