Make tab width 2 spaces (#445)

This commit is contained in:
Francesco Stasi
2021-07-09 10:14:42 +02:00
committed by GitHub
parent 40a73af82b
commit e10f0f1683
205 changed files with 19676 additions and 20141 deletions

View File

@@ -5,8 +5,8 @@ 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,
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';
@@ -19,285 +19,282 @@ import { getExecPath, spawnCommand } from './exec-util';
@injectable()
export class ArduinoDaemonImpl
implements ArduinoDaemon, BackendApplicationContribution
implements ArduinoDaemon, BackendApplicationContribution
{
@inject(ILogger)
@named('daemon')
protected readonly logger: ILogger;
@inject(ILogger)
@named('daemon')
protected readonly logger: ILogger;
@inject(EnvVariablesServer)
protected readonly envVariablesServer: EnvVariablesServer;
@inject(EnvVariablesServer)
protected readonly envVariablesServer: EnvVariablesServer;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
protected readonly toDispose = new DisposableCollection();
protected readonly onDaemonStartedEmitter = new Emitter<void>();
protected readonly onDaemonStoppedEmitter = new Emitter<void>();
protected readonly toDispose = new DisposableCollection();
protected readonly onDaemonStartedEmitter = new Emitter<void>();
protected readonly onDaemonStoppedEmitter = new Emitter<void>();
protected _running = false;
protected _ready = new Deferred<void>();
protected _execPath: string | undefined;
protected _running = false;
protected _ready = new Deferred<void>();
protected _execPath: string | undefined;
// Backend application lifecycle.
// Backend application lifecycle.
onStart(): void {
this.startDaemon();
}
onStart(): void {
this.startDaemon();
}
// Daemon API
// Daemon API
async isRunning(): Promise<boolean> {
return Promise.resolve(this._running);
}
async isRunning(): Promise<boolean> {
return Promise.resolve(this._running);
}
async startDaemon(): Promise<void> {
try {
this.toDispose.dispose(); // This will `kill` the previously started daemon process, if any.
const cliPath = await this.getExecPath();
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();
this.toDispose.pushAll([
Disposable.create(() => daemon.kill()),
Disposable.create(() => this.fireDaemonStopped()),
]);
this.fireDaemonStarted();
this.onData('Daemon is running.');
} catch (err) {
this.onData('Failed to start the daemon.');
this.onError(err);
let i = 5; // TODO: make this better
while (i) {
this.onData(`Restarting daemon in ${i} seconds...`);
await new Promise((resolve) => setTimeout(resolve, 1000));
i--;
}
this.onData('Restarting daemon now...');
return this.startDaemon();
async startDaemon(): Promise<void> {
try {
this.toDispose.dispose(); // This will `kill` the previously started daemon process, if any.
const cliPath = await this.getExecPath();
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();
async stopDaemon(): Promise<void> {
this.toDispose.dispose();
this.toDispose.pushAll([
Disposable.create(() => daemon.kill()),
Disposable.create(() => this.fireDaemonStopped()),
]);
this.fireDaemonStarted();
this.onData('Daemon is running.');
} catch (err) {
this.onData('Failed to start the daemon.');
this.onError(err);
let i = 5; // TODO: make this better
while (i) {
this.onData(`Restarting daemon in ${i} seconds...`);
await new Promise((resolve) => setTimeout(resolve, 1000));
i--;
}
this.onData('Restarting daemon now...');
return this.startDaemon();
}
}
get onDaemonStarted(): Event<void> {
return this.onDaemonStartedEmitter.event;
async stopDaemon(): Promise<void> {
this.toDispose.dispose();
}
get onDaemonStarted(): Event<void> {
return this.onDaemonStartedEmitter.event;
}
get onDaemonStopped(): Event<void> {
return this.onDaemonStoppedEmitter.event;
}
get ready(): Promise<void> {
return this._ready.promise;
}
async getExecPath(): Promise<string> {
if (this._execPath) {
return this._execPath;
}
this._execPath = await getExecPath('arduino-cli', this.onError.bind(this));
return this._execPath;
}
get onDaemonStopped(): Event<void> {
return this.onDaemonStoppedEmitter.event;
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)
);
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,
};
} catch {
return { version: raw, commit: raw };
}
}
get ready(): Promise<void> {
return this._ready.promise;
}
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',
];
}
async getExecPath(): Promise<string> {
if (this._execPath) {
return this._execPath;
protected async spawnDaemonProcess(): Promise<ChildProcess> {
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.)
// 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) => {
const message = data.toString();
this.onData(message);
if (!grpcServerIsReady) {
const error = DaemonError.parse(message);
if (error) {
ready.reject(error);
}
this._execPath = await getExecPath(
'arduino-cli',
this.onError.bind(this)
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);
}
}
}
});
daemon.stderr.on('data', (data) => {
const message = data.toString();
this.onData(data.toString());
const error = DaemonError.parse(message);
ready.reject(error ? error : new Error(data.toString().trim()));
});
daemon.on('exit', (code, signal) => {
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}`
}.`
);
return this._execPath;
}
});
daemon.on('error', (error) => {
this.onError(error);
ready.reject(error);
});
return ready.promise;
}
protected fireDaemonStarted(): void {
this._running = true;
this._ready.resolve();
this.onDaemonStartedEmitter.fire();
this.notificationService.notifyDaemonStarted();
}
protected fireDaemonStopped(): void {
if (!this._running) {
return;
}
this._running = false;
this._ready.reject(); // Reject all pending.
this._ready = new Deferred<void>();
this.onDaemonStoppedEmitter.fire();
this.notificationService.notifyDaemonStopped();
}
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)
);
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,
};
} catch {
return { version: raw, commit: raw };
}
}
protected onData(message: string): void {
DaemonLog.log(this.logger, message);
}
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',
];
}
protected async spawnDaemonProcess(): Promise<ChildProcess> {
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.)
// 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) => {
const message = data.toString();
this.onData(message);
if (!grpcServerIsReady) {
const error = DaemonError.parse(message);
if (error) {
ready.reject(error);
}
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);
}
}
}
});
daemon.stderr.on('data', (data) => {
const message = data.toString();
this.onData(data.toString());
const error = DaemonError.parse(message);
ready.reject(error ? error : new Error(data.toString().trim()));
});
daemon.on('exit', (code, signal) => {
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}`
}.`
);
}
});
daemon.on('error', (error) => {
this.onError(error);
ready.reject(error);
});
return ready.promise;
}
protected fireDaemonStarted(): void {
this._running = true;
this._ready.resolve();
this.onDaemonStartedEmitter.fire();
this.notificationService.notifyDaemonStarted();
}
protected fireDaemonStopped(): void {
if (!this._running) {
return;
}
this._running = false;
this._ready.reject(); // Reject all pending.
this._ready = new Deferred<void>();
this.onDaemonStoppedEmitter.fire();
this.notificationService.notifyDaemonStopped();
}
protected onData(message: string): void {
DaemonLog.log(this.logger, message);
}
protected onError(error: any): void {
this.logger.error(error);
}
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
) {
super(message);
Object.setPrototypeOf(this, DaemonError.prototype);
}
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;
export const UNKNOWN = 8;
export const ADDRESS_IN_USE = 0;
export const UNKNOWN_ADDRESS = 2;
export const INVALID_PORT = 4;
export const UNKNOWN = 8;
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('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
);
}
}
// 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
);
}
return undefined;
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('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
);
}
}
// 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
);
}
return undefined;
}
}

View File

@@ -2,16 +2,16 @@ import { ContainerModule } from 'inversify';
import { ArduinoDaemonImpl } from './arduino-daemon-impl';
import { ILogger } from '@theia/core/lib/common/logger';
import {
BackendApplicationContribution,
BackendApplication as TheiaBackendApplication,
BackendApplicationContribution,
BackendApplication as TheiaBackendApplication,
} from '@theia/core/lib/node/backend-application';
import {
LibraryService,
LibraryServicePath,
LibraryService,
LibraryServicePath,
} from '../common/protocol/library-service';
import {
BoardsService,
BoardsServicePath,
BoardsService,
BoardsServicePath,
} from '../common/protocol/boards-service';
import { LibraryServiceImpl } from './library-service-server-impl';
import { BoardsServiceImpl } from './boards-service-impl';
@@ -24,22 +24,22 @@ import { DefaultWorkspaceServer } from './theia/workspace/default-workspace-serv
import { WorkspaceServer as TheiaWorkspaceServer } from '@theia/workspace/lib/common';
import { SketchesServiceImpl } from './sketches-service-impl';
import {
SketchesService,
SketchesServicePath,
SketchesService,
SketchesServicePath,
} from '../common/protocol/sketches-service';
import {
ConfigService,
ConfigServicePath,
ConfigService,
ConfigServicePath,
} from '../common/protocol/config-service';
import {
ArduinoDaemon,
ArduinoDaemonPath,
ArduinoDaemon,
ArduinoDaemonPath,
} from '../common/protocol/arduino-daemon';
import { MonitorServiceImpl } from './monitor/monitor-service-impl';
import {
MonitorService,
MonitorServicePath,
MonitorServiceClient,
MonitorService,
MonitorServicePath,
MonitorServiceClient,
} from '../common/protocol/monitor-service';
import { MonitorClientProvider } from './monitor/monitor-client-provider';
import { ConfigServiceImpl } from './config-service-impl';
@@ -47,28 +47,28 @@ import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/c
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
import { NodeFileSystemExt } from './node-filesystem-ext';
import {
FileSystemExt,
FileSystemExtPath,
FileSystemExt,
FileSystemExtPath,
} from '../common/protocol/filesystem-ext';
import { ExamplesServiceImpl } from './examples-service-impl';
import {
ExamplesService,
ExamplesServicePath,
ExamplesService,
ExamplesServicePath,
} from '../common/protocol/examples-service';
import {
ExecutableService,
ExecutableServicePath,
ExecutableService,
ExecutableServicePath,
} from '../common/protocol/executable-service';
import { ExecutableServiceImpl } from './executable-service-impl';
import {
ResponseServicePath,
ResponseService,
ResponseServicePath,
ResponseService,
} from '../common/protocol/response-service';
import { NotificationServiceServerImpl } from './notification-service-server';
import {
NotificationServiceServer,
NotificationServiceClient,
NotificationServicePath,
NotificationServiceServer,
NotificationServiceClient,
NotificationServicePath,
} from '../common/protocol';
import { BackendApplication } from './theia/core/backend-application';
import { BoardDiscovery } from './board-discovery';
@@ -76,238 +76,232 @@ import { DefaultGitInit } from './theia/git/git-init';
import { GitInit } from '@theia/git/lib/node/init/git-init';
import { AuthenticationServiceImpl } from './auth/authentication-service-impl';
import {
AuthenticationService,
AuthenticationServiceClient,
AuthenticationServicePath,
AuthenticationService,
AuthenticationServiceClient,
AuthenticationServicePath,
} from '../common/protocol/authentication-service';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope();
rebind(TheiaBackendApplication).toService(BackendApplication);
bind(BackendApplication).toSelf().inSingletonScope();
rebind(TheiaBackendApplication).toService(BackendApplication);
// Shared config service
bind(ConfigServiceImpl).toSelf().inSingletonScope();
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)
)
// Shared config service
bind(ConfigServiceImpl).toSelf().inSingletonScope();
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();
)
.inSingletonScope();
// 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)
)
// 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();
)
.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);
})
);
// 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);
})
);
// 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)
)
// 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();
)
.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);
})
);
// 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);
})
);
// Shared sketches service
bind(SketchesServiceImpl).toSelf().inSingletonScope();
bind(SketchesService).toService(SketchesServiceImpl);
bind(ConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(SketchesServicePath, () =>
context.container.get(SketchesService)
)
// Shared sketches service
bind(SketchesServiceImpl).toSelf().inSingletonScope();
bind(SketchesService).toService(SketchesServiceImpl);
bind(ConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(SketchesServicePath, () =>
context.container.get(SketchesService)
)
.inSingletonScope();
)
.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);
})
);
// 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);
})
);
// Shared Arduino core client provider service for the backend.
bind(CoreClientProvider).toSelf().inSingletonScope();
// Shared Arduino core client provider service for the backend.
bind(CoreClientProvider).toSelf().inSingletonScope();
// Shared port/board discovery for the server
bind(BoardDiscovery).toSelf().inSingletonScope();
// Shared port/board discovery for the server
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);
})
);
// 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);
})
);
// #region Theia customizations
// #region Theia customizations
bind(DefaultWorkspaceServer).toSelf().inSingletonScope();
rebind(TheiaWorkspaceServer).toService(DefaultWorkspaceServer);
bind(DefaultWorkspaceServer).toSelf().inSingletonScope();
rebind(TheiaWorkspaceServer).toService(DefaultWorkspaceServer);
bind(EnvVariablesServer).toSelf().inSingletonScope();
rebind(TheiaEnvVariablesServer).toService(EnvVariablesServer);
bind(EnvVariablesServer).toSelf().inSingletonScope();
rebind(TheiaEnvVariablesServer).toService(EnvVariablesServer);
// #endregion Theia customizations
// #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;
}
// 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;
}
);
})
);
// 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();
// Output service per connection.
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
);
})
);
// 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)
)
server.setClient(client);
client.onDidCloseConnection(() => server.disposeClient(client));
return server;
}
)
.inSingletonScope();
)
.inSingletonScope();
// Output service per connection.
bind(ConnectionContainerModule).toConstantValue(
ConnectionContainerModule.create(({ bindFrontendService }) => {
bindFrontendService(ResponseServicePath, ResponseService);
})
);
// Logger for the Arduino daemon
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('daemon');
})
.inSingletonScope()
.whenTargetNamed('daemon');
// 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;
}
)
// Logger for the "serial 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');
// 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(DefaultGitInit).toSelf();
rebind(GitInit).toService(DefaultGitInit);
// Remote sketchbook bindings
bind(AuthenticationServiceImpl).toSelf().inSingletonScope();
bind(AuthenticationService).toService(AuthenticationServiceImpl);
bind(BackendApplicationContribution).toService(AuthenticationServiceImpl);
bind(ConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler<AuthenticationServiceClient>(
AuthenticationServicePath,
(client) => {
const server = context.container.get<AuthenticationServiceImpl>(
AuthenticationServiceImpl
);
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');
// Logger for the "serial 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');
// 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(DefaultGitInit).toSelf();
rebind(GitInit).toService(DefaultGitInit);
// Remote sketchbook bindings
bind(AuthenticationServiceImpl).toSelf().inSingletonScope();
bind(AuthenticationService).toService(AuthenticationServiceImpl);
bind(BackendApplicationContribution).toService(AuthenticationServiceImpl);
bind(ConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler<AuthenticationServiceClient>(
AuthenticationServicePath,
(client) => {
const server =
context.container.get<AuthenticationServiceImpl>(
AuthenticationServiceImpl
);
server.setClient(client);
client.onDidCloseConnection(() =>
server.disposeClient(client)
);
return server;
}
)
)
.inSingletonScope();
)
.inSingletonScope();
});

View File

@@ -4,19 +4,19 @@ import { injectable } from 'inversify';
import { createServer, startServer } from './authentication-server';
import { Keychain } from './keychain';
import {
generateProofKeyPair,
Token,
IToken,
IToken2Session,
token2IToken,
RefreshToken,
generateProofKeyPair,
Token,
IToken,
IToken2Session,
token2IToken,
RefreshToken,
} from './utils';
import { Authentication } from 'auth0-js';
import {
AuthenticationProviderAuthenticationSessionsChangeEvent,
AuthenticationSession,
AuthenticationProvider,
AuthOptions,
AuthenticationProviderAuthenticationSessionsChangeEvent,
AuthenticationSession,
AuthenticationProvider,
AuthOptions,
} from './types';
import { Event, Emitter } from '@theia/core/lib/common/event';
import * as open from 'open';
@@ -26,319 +26,312 @@ const REFRESH_INTERVAL = 10 * 60 * 1000;
@injectable()
export class ArduinoAuthenticationProvider implements AuthenticationProvider {
protected authOptions: AuthOptions;
protected authOptions: AuthOptions;
public readonly id: string = 'arduino-account-auth';
public readonly label = 'Arduino';
public readonly id: string = 'arduino-account-auth';
public readonly label = 'Arduino';
// create a keychain holding the keys
private keychain = new Keychain({
credentialsSection: this.id,
account: this.label,
});
// create a keychain holding the keys
private keychain = new Keychain({
credentialsSection: this.id,
account: this.label,
});
private _tokens: IToken[] = [];
private _refreshTimeouts: Map<string, NodeJS.Timeout> = new Map<
string,
NodeJS.Timeout
>();
private _tokens: IToken[] = [];
private _refreshTimeouts: Map<string, NodeJS.Timeout> = new Map<
string,
NodeJS.Timeout
>();
private _onDidChangeSessions =
new Emitter<AuthenticationProviderAuthenticationSessionsChangeEvent>();
public get onDidChangeSessions(): Event<AuthenticationProviderAuthenticationSessionsChangeEvent> {
return this._onDidChangeSessions.event;
private _onDidChangeSessions =
new Emitter<AuthenticationProviderAuthenticationSessionsChangeEvent>();
public get onDidChangeSessions(): Event<AuthenticationProviderAuthenticationSessionsChangeEvent> {
return this._onDidChangeSessions.event;
}
private get sessions(): Promise<AuthenticationSession[]> {
return Promise.resolve(this._tokens.map((token) => IToken2Session(token)));
}
public getSessions(): Promise<AuthenticationSession[]> {
return Promise.resolve(this.sessions);
}
public async init(): Promise<void> {
// restore previously stored sessions
const stringTokens = await this.keychain.getStoredCredentials();
// no valid token, nothing to do
if (!stringTokens) {
return;
}
private get sessions(): Promise<AuthenticationSession[]> {
return Promise.resolve(
this._tokens.map((token) => IToken2Session(token))
const checkToken = async () => {
// tokens exist, parse and refresh them
try {
const tokens: IToken[] = JSON.parse(stringTokens);
// refresh the tokens when needed
await Promise.all(
tokens.map(async (token) => {
// if refresh not needed, add the existing token
if (!IToken.requiresRefresh(token, REFRESH_INTERVAL)) {
return this.addToken(token);
}
const refreshedToken = await this.refreshToken(token);
return this.addToken(refreshedToken);
})
);
} catch {
return;
}
};
checkToken();
setInterval(checkToken, REFRESH_INTERVAL);
}
public setOptions(authOptions: AuthOptions) {
this.authOptions = authOptions;
}
public dispose(): void {}
public async refreshToken(token: IToken): Promise<IToken> {
if (!token.refreshToken) {
throw new Error('Unable to refresh a token without a refreshToken');
}
public getSessions(): Promise<AuthenticationSession[]> {
return Promise.resolve(this.sessions);
console.log(`Refreshing token ${token.sessionId}`);
const response = await fetch(
`https://${this.authOptions.domain}/oauth/token`,
{
method: 'POST',
body: JSON.stringify({
grant_type: 'refresh_token',
client_id: this.authOptions.clientID,
refresh_token: token.refreshToken,
}),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
if (response.ok) {
const result: RefreshToken = await response.json();
// add the refresh_token from the old token
return token2IToken({
...result,
refresh_token: token.refreshToken,
});
}
throw new Error(`Failed to refresh a token: ${response.statusText}`);
}
public async init(): Promise<void> {
// restore previously stored sessions
const stringTokens = await this.keychain.getStoredCredentials();
private async exchangeCodeForToken(
authCode: string,
verifier: string
): Promise<Token> {
const response = await fetch(
`https://${this.authOptions.domain}/oauth/token`,
{
method: 'POST',
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: this.authOptions.clientID,
code_verifier: verifier,
code: authCode,
redirect_uri: this.authOptions.redirectUri,
}),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
if (response.ok) {
return await response.json();
}
throw new Error(`Failed to fetch a token: ${response.statusText}`);
}
// no valid token, nothing to do
if (!stringTokens) {
return;
public async createSession(): Promise<AuthenticationSession> {
const token = await this.login();
this.addToken(token);
return IToken2Session(token);
}
private async login(): Promise<IToken> {
return new Promise<IToken>(async (resolve, reject) => {
const pkp = generateProofKeyPair();
const server = createServer(async (req, res) => {
const { url } = req;
if (url && url.startsWith('/callback?code=')) {
const code = url.slice('/callback?code='.length);
const token = await this.exchangeCodeForToken(code, pkp.verifier);
resolve(token2IToken(token));
}
const checkToken = async () => {
// tokens exist, parse and refresh them
try {
const tokens: IToken[] = JSON.parse(stringTokens);
// refresh the tokens when needed
await Promise.all(
tokens.map(async (token) => {
// if refresh not needed, add the existing token
if (!IToken.requiresRefresh(token, REFRESH_INTERVAL)) {
return this.addToken(token);
}
const refreshedToken = await this.refreshToken(token);
return this.addToken(refreshedToken);
})
);
} catch {
return;
}
};
checkToken();
setInterval(checkToken, REFRESH_INTERVAL);
}
// schedule server shutdown after 10 seconds
setTimeout(() => {
server.close();
}, LOGIN_TIMEOUT);
});
public setOptions(authOptions: AuthOptions) {
this.authOptions = authOptions;
}
try {
const port = await startServer(server);
console.log(`server listening on http://localhost:${port}`);
public dispose(): void {}
public async refreshToken(token: IToken): Promise<IToken> {
if (!token.refreshToken) {
throw new Error('Unable to refresh a token without a refreshToken');
}
console.log(`Refreshing token ${token.sessionId}`);
const response = await fetch(
`https://${this.authOptions.domain}/oauth/token`,
{
method: 'POST',
body: JSON.stringify({
grant_type: 'refresh_token',
client_id: this.authOptions.clientID,
refresh_token: token.refreshToken,
}),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
if (response.ok) {
const result: RefreshToken = await response.json();
// add the refresh_token from the old token
return token2IToken({
...result,
refresh_token: token.refreshToken,
});
}
throw new Error(`Failed to refresh a token: ${response.statusText}`);
}
private async exchangeCodeForToken(
authCode: string,
verifier: string
): Promise<Token> {
const response = await fetch(
`https://${this.authOptions.domain}/oauth/token`,
{
method: 'POST',
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: this.authOptions.clientID,
code_verifier: verifier,
code: authCode,
redirect_uri: this.authOptions.redirectUri,
}),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
if (response.ok) {
return await response.json();
}
throw new Error(`Failed to fetch a token: ${response.statusText}`);
}
public async createSession(): Promise<AuthenticationSession> {
const token = await this.login();
this.addToken(token);
return IToken2Session(token);
}
private async login(): Promise<IToken> {
return new Promise<IToken>(async (resolve, reject) => {
const pkp = generateProofKeyPair();
const server = createServer(async (req, res) => {
const { url } = req;
if (url && url.startsWith('/callback?code=')) {
const code = url.slice('/callback?code='.length);
const token = await this.exchangeCodeForToken(
code,
pkp.verifier
);
resolve(token2IToken(token));
}
// schedule server shutdown after 10 seconds
setTimeout(() => {
server.close();
}, LOGIN_TIMEOUT);
});
try {
const port = await startServer(server);
console.log(`server listening on http://localhost:${port}`);
const auth0 = new Authentication({
clientID: this.authOptions.clientID,
domain: this.authOptions.domain,
audience: this.authOptions.audience,
redirectUri: `http://localhost:${port}/callback`,
scope: this.authOptions.scopes.join(' '),
responseType: this.authOptions.responseType,
code_challenge_method: 'S256',
code_challenge: pkp.challenge,
} as any);
const authorizeUrl = auth0.buildAuthorizeUrl({
redirectUri: `http://localhost:${port}/callback`,
responseType: this.authOptions.responseType,
});
await open(authorizeUrl);
// set a timeout if the authentication takes too long
setTimeout(() => {
server.close();
reject(new Error('Login timeout.'));
}, 30000);
} finally {
// server is usually closed by the callback or the timeout, this is to handle corner cases
setTimeout(() => {
server.close();
}, 50000);
}
const auth0 = new Authentication({
clientID: this.authOptions.clientID,
domain: this.authOptions.domain,
audience: this.authOptions.audience,
redirectUri: `http://localhost:${port}/callback`,
scope: this.authOptions.scopes.join(' '),
responseType: this.authOptions.responseType,
code_challenge_method: 'S256',
code_challenge: pkp.challenge,
} as any);
const authorizeUrl = auth0.buildAuthorizeUrl({
redirectUri: `http://localhost:${port}/callback`,
responseType: this.authOptions.responseType,
});
await open(authorizeUrl);
// set a timeout if the authentication takes too long
setTimeout(() => {
server.close();
reject(new Error('Login timeout.'));
}, 30000);
} finally {
// server is usually closed by the callback or the timeout, this is to handle corner cases
setTimeout(() => {
server.close();
}, 50000);
}
});
}
public async signUp(): Promise<void> {
await open(this.authOptions.registerUri);
}
/**
* Returns extended account info for the given (and logged-in) sessionId.
*
* @param sessionId the sessionId to get info about. If not provided, all account info are returned
* @returns an array of IToken, containing extended info for the accounts
*/
public accountInfo(sessionId?: string) {
return this._tokens.filter((token) =>
sessionId ? token.sessionId === sessionId : true
);
}
/**
* Removes any logged-in sessions
*/
public logout() {
this._tokens.forEach((token) => this.removeSession(token.sessionId));
// remove any dangling credential in the keychain
this.keychain.deleteCredentials();
}
public async removeSession(sessionId: string): Promise<void> {
// remove token from memory, if successful fire the event
const token = this.removeInMemoryToken(sessionId);
if (token) {
this._onDidChangeSessions.fire({
added: [],
removed: [IToken2Session(token)],
changed: [],
});
}
public async signUp(): Promise<void> {
await open(this.authOptions.registerUri);
// update the tokens in the keychain
this.keychain.storeCredentials(JSON.stringify(this._tokens));
}
/**
* Clears the refresh timeout associated to a session and removes the key from the set
*/
private clearSessionTimeout(sessionId: string): void {
const timeout = this._refreshTimeouts.get(sessionId);
if (timeout) {
clearTimeout(timeout);
this._refreshTimeouts.delete(sessionId);
}
}
/**
* Remove the given token from memory and clears the associated refresh timeout
* @param token
* @returns the removed token
*/
private removeInMemoryToken(sessionId: string): IToken | undefined {
const tokenIndex = this._tokens.findIndex(
(token) => token.sessionId === sessionId
);
let token: IToken | undefined;
if (tokenIndex > -1) {
token = this._tokens[tokenIndex];
this._tokens.splice(tokenIndex, 1);
}
/**
* Returns extended account info for the given (and logged-in) sessionId.
*
* @param sessionId the sessionId to get info about. If not provided, all account info are returned
* @returns an array of IToken, containing extended info for the accounts
*/
public accountInfo(sessionId?: string) {
return this._tokens.filter((token) =>
sessionId ? token.sessionId === sessionId : true
);
this.clearSessionTimeout(sessionId);
return token;
}
/**
* Add the given token to memory storage and keychain. Prepares Timeout for token refresh
* NOTE: we currently support 1 token (logged user) at a time
* @param token
* @returns
*/
public async addToken(token: IToken): Promise<IToken | undefined> {
if (!token) {
return;
}
this._tokens = [token];
// update the tokens in the keychain
this.keychain.storeCredentials(JSON.stringify(this._tokens));
// notify subscribers about the newly added/changed session
const session = IToken2Session(token);
const changedToken = this._tokens.find(
(itoken) => itoken.sessionId === session.id
);
const changes = {
added: (!changedToken && [session]) || [],
removed: [],
changed: (!!changedToken && [session]) || [],
};
this._onDidChangeSessions.fire(changes);
// setup token refresh
this.clearSessionTimeout(token.sessionId);
if (token.expiresAt) {
// refresh the token 30sec before expiration
const expiration = token.expiresAt - Date.now() - 30 * 1000;
this._refreshTimeouts.set(
token.sessionId,
setTimeout(
async () => {
try {
const refreshedToken = await this.refreshToken(token);
this.addToken(refreshedToken);
} catch (e) {
await this.removeSession(token.sessionId);
}
},
expiration > 0 ? expiration : 0
)
);
}
/**
* Removes any logged-in sessions
*/
public logout() {
this._tokens.forEach((token) => this.removeSession(token.sessionId));
// remove any dangling credential in the keychain
this.keychain.deleteCredentials();
}
public async removeSession(sessionId: string): Promise<void> {
// remove token from memory, if successful fire the event
const token = this.removeInMemoryToken(sessionId);
if (token) {
this._onDidChangeSessions.fire({
added: [],
removed: [IToken2Session(token)],
changed: [],
});
}
// update the tokens in the keychain
this.keychain.storeCredentials(JSON.stringify(this._tokens));
}
/**
* Clears the refresh timeout associated to a session and removes the key from the set
*/
private clearSessionTimeout(sessionId: string): void {
const timeout = this._refreshTimeouts.get(sessionId);
if (timeout) {
clearTimeout(timeout);
this._refreshTimeouts.delete(sessionId);
}
}
/**
* Remove the given token from memory and clears the associated refresh timeout
* @param token
* @returns the removed token
*/
private removeInMemoryToken(sessionId: string): IToken | undefined {
const tokenIndex = this._tokens.findIndex(
(token) => token.sessionId === sessionId
);
let token: IToken | undefined;
if (tokenIndex > -1) {
token = this._tokens[tokenIndex];
this._tokens.splice(tokenIndex, 1);
}
this.clearSessionTimeout(sessionId);
return token;
}
/**
* Add the given token to memory storage and keychain. Prepares Timeout for token refresh
* NOTE: we currently support 1 token (logged user) at a time
* @param token
* @returns
*/
public async addToken(token: IToken): Promise<IToken | undefined> {
if (!token) {
return;
}
this._tokens = [token];
// update the tokens in the keychain
this.keychain.storeCredentials(JSON.stringify(this._tokens));
// notify subscribers about the newly added/changed session
const session = IToken2Session(token);
const changedToken = this._tokens.find(
(itoken) => itoken.sessionId === session.id
);
const changes = {
added: (!changedToken && [session]) || [],
removed: [],
changed: (!!changedToken && [session]) || [],
};
this._onDidChangeSessions.fire(changes);
// setup token refresh
this.clearSessionTimeout(token.sessionId);
if (token.expiresAt) {
// refresh the token 30sec before expiration
const expiration = token.expiresAt - Date.now() - 30 * 1000;
this._refreshTimeouts.set(
token.sessionId,
setTimeout(
async () => {
try {
const refreshedToken = await this.refreshToken(
token
);
this.addToken(refreshedToken);
} catch (e) {
await this.removeSession(token.sessionId);
}
},
expiration > 0 ? expiration : 0
)
);
}
return token;
}
return token;
}
}

View File

@@ -6,62 +6,62 @@ export const authCallbackPath = 'callback';
export const serverPort = 9876;
export function createServer(
authCallback: (req: http.IncomingMessage, res: http.ServerResponse) => void
authCallback: (req: http.IncomingMessage, res: http.ServerResponse) => void
) {
const server = http.createServer(function (req, res) {
const reqUrl = url.parse(req.url!, /* parseQueryString */ true);
switch (reqUrl.pathname) {
case `/${authCallbackPath}`:
authCallback(req, res);
res.writeHead(200, {
'Content-Length': body.length,
'Content-Type': 'text/html; charset=utf-8',
});
res.end(body);
break;
default:
res.writeHead(404);
res.end();
break;
}
});
return server;
const server = http.createServer(function (req, res) {
const reqUrl = url.parse(req.url!, /* parseQueryString */ true);
switch (reqUrl.pathname) {
case `/${authCallbackPath}`:
authCallback(req, res);
res.writeHead(200, {
'Content-Length': body.length,
'Content-Type': 'text/html; charset=utf-8',
});
res.end(body);
break;
default:
res.writeHead(404);
res.end();
break;
}
});
return server;
}
export async function startServer(server: http.Server): Promise<string> {
let portTimer: NodeJS.Timer;
let portTimer: NodeJS.Timer;
function cancelPortTimer() {
clearTimeout(portTimer);
}
function cancelPortTimer() {
clearTimeout(portTimer);
}
const port = new Promise<string>((resolve, reject) => {
portTimer = setTimeout(() => {
reject(new Error('Timeout waiting for port'));
}, 5000);
const port = new Promise<string>((resolve, reject) => {
portTimer = setTimeout(() => {
reject(new Error('Timeout waiting for port'));
}, 5000);
server.on('listening', () => {
const address = server.address();
if (typeof address === 'undefined' || address === null) {
reject(new Error('address is null or undefined'));
} else if (typeof address === 'string') {
resolve(address);
} else {
resolve(address.port.toString());
}
});
server.on('error', (_) => {
reject(new Error('Error listening to server'));
});
server.on('close', () => {
reject(new Error('Closed'));
});
server.listen(serverPort);
server.on('listening', () => {
const address = server.address();
if (typeof address === 'undefined' || address === null) {
reject(new Error('address is null or undefined'));
} else if (typeof address === 'string') {
resolve(address);
} else {
resolve(address.port.toString());
}
});
port.then(cancelPortTimer, cancelPortTimer);
return port;
server.on('error', (_) => {
reject(new Error('Error listening to server'));
});
server.on('close', () => {
reject(new Error('Closed'));
});
server.listen(serverPort);
});
port.then(cancelPortTimer, cancelPortTimer);
return port;
}

View File

@@ -1,87 +1,85 @@
import { injectable } from 'inversify';
import {
Disposable,
DisposableCollection,
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import {
AuthenticationService,
AuthenticationServiceClient,
AuthenticationSession,
AuthenticationService,
AuthenticationServiceClient,
AuthenticationSession,
} from '../../common/protocol/authentication-service';
import { ArduinoAuthenticationProvider } from './arduino-auth-provider';
import { AuthOptions } from './types';
@injectable()
export class AuthenticationServiceImpl
implements AuthenticationService, BackendApplicationContribution
implements AuthenticationService, BackendApplicationContribution
{
protected readonly delegate = new ArduinoAuthenticationProvider();
protected readonly clients: AuthenticationServiceClient[] = [];
protected readonly toDispose = new DisposableCollection();
protected readonly delegate = new ArduinoAuthenticationProvider();
protected readonly clients: AuthenticationServiceClient[] = [];
protected readonly toDispose = new DisposableCollection();
async onStart(): Promise<void> {
this.toDispose.pushAll([
this.delegate,
this.delegate.onDidChangeSessions(({ added, removed, changed }) => {
added?.forEach((session) =>
this.clients.forEach((client) =>
client.notifySessionDidChange(session)
)
);
changed?.forEach((session) =>
this.clients.forEach((client) =>
client.notifySessionDidChange(session)
)
);
removed?.forEach(() =>
this.clients.forEach((client) =>
client.notifySessionDidChange()
)
);
}),
Disposable.create(() =>
this.clients.forEach((client) => this.disposeClient(client))
),
]);
await this.delegate.init();
}
async onStart(): Promise<void> {
this.toDispose.pushAll([
this.delegate,
this.delegate.onDidChangeSessions(({ added, removed, changed }) => {
added?.forEach((session) =>
this.clients.forEach((client) =>
client.notifySessionDidChange(session)
)
);
changed?.forEach((session) =>
this.clients.forEach((client) =>
client.notifySessionDidChange(session)
)
);
removed?.forEach(() =>
this.clients.forEach((client) => client.notifySessionDidChange())
);
}),
Disposable.create(() =>
this.clients.forEach((client) => this.disposeClient(client))
),
]);
await this.delegate.init();
}
setOptions(authOptions: AuthOptions) {
this.delegate.setOptions(authOptions);
}
setOptions(authOptions: AuthOptions) {
this.delegate.setOptions(authOptions);
}
async login(): Promise<AuthenticationSession> {
return this.delegate.createSession();
}
async login(): Promise<AuthenticationSession> {
return this.delegate.createSession();
}
async logout(): Promise<void> {
this.delegate.logout();
}
async logout(): Promise<void> {
this.delegate.logout();
}
async session(): Promise<AuthenticationSession | undefined> {
const sessions = await this.delegate.getSessions();
return sessions[0];
}
async session(): Promise<AuthenticationSession | undefined> {
const sessions = await this.delegate.getSessions();
return sessions[0];
}
dispose(): void {
this.toDispose.dispose();
}
dispose(): void {
this.toDispose.dispose();
}
setClient(client: AuthenticationServiceClient | undefined): void {
if (client) {
this.clients.push(client);
}
setClient(client: AuthenticationServiceClient | undefined): void {
if (client) {
this.clients.push(client);
}
}
disposeClient(client: AuthenticationServiceClient) {
const index = this.clients.indexOf(client);
if (index === -1) {
console.warn(
'Could not dispose authentications service client. It was not registered. Skipping.'
);
return;
}
this.clients.splice(index, 1);
disposeClient(client: AuthenticationServiceClient) {
const index = this.clients.indexOf(client);
if (index === -1) {
console.warn(
'Could not dispose authentications service client. It was not registered. Skipping.'
);
return;
}
this.clients.splice(index, 1);
}
}

View File

@@ -1,76 +1,76 @@
import type * as keytarType from 'keytar';
export type KeychainConfig = {
credentialsSection: string;
account: string;
credentialsSection: string;
account: string;
};
type Keytar = {
getPassword: typeof keytarType['getPassword'];
setPassword: typeof keytarType['setPassword'];
deletePassword: typeof keytarType['deletePassword'];
getPassword: typeof keytarType['getPassword'];
setPassword: typeof keytarType['setPassword'];
deletePassword: typeof keytarType['deletePassword'];
};
export class Keychain {
credentialsSection: string;
account: string;
credentialsSection: string;
account: string;
constructor(config: KeychainConfig) {
this.credentialsSection = config.credentialsSection;
this.account = config.account;
}
constructor(config: KeychainConfig) {
this.credentialsSection = config.credentialsSection;
this.account = config.account;
}
getKeytar(): Keytar | undefined {
try {
return require('keytar');
} catch (err) {
console.log(err);
}
return undefined;
getKeytar(): Keytar | undefined {
try {
return require('keytar');
} catch (err) {
console.log(err);
}
return undefined;
}
async getStoredCredentials(): Promise<string | undefined | null> {
const keytar = this.getKeytar();
if (!keytar) {
return undefined;
}
try {
return keytar.getPassword(this.credentialsSection, this.account);
} catch {
return undefined;
}
async getStoredCredentials(): Promise<string | undefined | null> {
const keytar = this.getKeytar();
if (!keytar) {
return undefined;
}
try {
return keytar.getPassword(this.credentialsSection, this.account);
} catch {
return undefined;
}
}
async storeCredentials(stringifiedToken: string): Promise<boolean> {
const keytar = this.getKeytar();
if (!keytar) {
return false;
}
try {
await keytar.setPassword(
this.credentialsSection,
this.account,
stringifiedToken
);
return true;
} catch {
return false;
}
async storeCredentials(stringifiedToken: string): Promise<boolean> {
const keytar = this.getKeytar();
if (!keytar) {
return false;
}
try {
await keytar.setPassword(
this.credentialsSection,
this.account,
stringifiedToken
);
return true;
} catch {
return false;
}
}
async deleteCredentials(): Promise<boolean> {
const keytar = this.getKeytar();
if (!keytar) {
return false;
}
try {
const result = await keytar.deletePassword(
this.credentialsSection,
this.account
);
return result;
} catch {
return false;
}
async deleteCredentials(): Promise<boolean> {
const keytar = this.getKeytar();
if (!keytar) {
return false;
}
try {
const result = await keytar.deletePassword(
this.credentialsSection,
this.account
);
return result;
} catch {
return false;
}
}
}

View File

@@ -2,25 +2,25 @@ import { AuthenticationSession } from '../../common/protocol/authentication-serv
export { AuthenticationSession };
export type AuthOptions = {
redirectUri: string;
responseType: string;
clientID: string;
domain: string;
audience: string;
registerUri: string;
scopes: string[];
redirectUri: string;
responseType: string;
clientID: string;
domain: string;
audience: string;
registerUri: string;
scopes: string[];
};
export interface AuthenticationProviderAuthenticationSessionsChangeEvent {
readonly added?: ReadonlyArray<AuthenticationSession>;
readonly removed?: ReadonlyArray<AuthenticationSession>;
readonly changed?: ReadonlyArray<AuthenticationSession>;
readonly added?: ReadonlyArray<AuthenticationSession>;
readonly removed?: ReadonlyArray<AuthenticationSession>;
readonly changed?: ReadonlyArray<AuthenticationSession>;
}
export interface AuthenticationProvider {
readonly onDidChangeSessions: any; // Event<AuthenticationProviderAuthenticationSessionsChangeEvent>;
getSessions(): Promise<ReadonlyArray<AuthenticationSession>>;
createSession(): Promise<AuthenticationSession>;
removeSession(sessionId: string): Promise<void>;
setOptions(authOptions: AuthOptions): void;
readonly onDidChangeSessions: any; // Event<AuthenticationProviderAuthenticationSessionsChangeEvent>;
getSessions(): Promise<ReadonlyArray<AuthenticationSession>>;
createSession(): Promise<AuthenticationSession>;
removeSession(sessionId: string): Promise<void>;
setOptions(authOptions: AuthOptions): void;
}

View File

@@ -5,101 +5,99 @@ import btoa = require('btoa'); // TODO: check why we cannot
import { AuthenticationSession } from './types';
export interface IToken {
accessToken: string; // When unable to refresh due to network problems, the access token becomes undefined
idToken?: string; // depending on the scopes can be either supplied or empty
accessToken: string; // When unable to refresh due to network problems, the access token becomes undefined
idToken?: string; // depending on the scopes can be either supplied or empty
expiresIn?: number; // How long access token is valid, in seconds
expiresAt?: number; // UNIX epoch time at which token will expire
refreshToken: string;
expiresIn?: number; // How long access token is valid, in seconds
expiresAt?: number; // UNIX epoch time at which token will expire
refreshToken: string;
account: {
id: string;
email: string;
nickname: string;
picture: string;
};
scope: string;
sessionId: string;
account: {
id: string;
email: string;
nickname: string;
picture: string;
};
scope: string;
sessionId: string;
}
export namespace IToken {
// check if the token is expired or will expired before the buffer
export function requiresRefresh(token: IToken, buffer: number): boolean {
return token.expiresAt ? token.expiresAt < Date.now() + buffer : false;
}
// check if the token is expired or will expired before the buffer
export function requiresRefresh(token: IToken, buffer: number): boolean {
return token.expiresAt ? token.expiresAt < Date.now() + buffer : false;
}
}
export interface Token {
access_token: string;
id_token?: string;
refresh_token: string;
scope: 'offline_access' | string; // `offline_access`
expires_in: number; // expires in seconds
token_type: string; // `Bearer`
access_token: string;
id_token?: string;
refresh_token: string;
scope: 'offline_access' | string; // `offline_access`
expires_in: number; // expires in seconds
token_type: string; // `Bearer`
}
export type RefreshToken = Omit<Token, 'refresh_token'>;
export function token2IToken(token: Token): IToken {
const parsedIdToken: any =
(token.id_token && jwt_decode(token.id_token)) || {};
const parsedIdToken: any =
(token.id_token && jwt_decode(token.id_token)) || {};
return {
idToken: token.id_token,
expiresIn: token.expires_in,
expiresAt: token.expires_in
? Date.now() + token.expires_in * 1000
: undefined,
accessToken: token.access_token,
refreshToken: token.refresh_token,
sessionId: parsedIdToken.sub,
scope: token.scope,
account: {
id: parsedIdToken.sub || 'unknown',
email: parsedIdToken.email || 'unknown',
nickname: parsedIdToken.nickname || 'unknown',
picture: parsedIdToken.picture || 'unknown',
},
};
return {
idToken: token.id_token,
expiresIn: token.expires_in,
expiresAt: token.expires_in
? Date.now() + token.expires_in * 1000
: undefined,
accessToken: token.access_token,
refreshToken: token.refresh_token,
sessionId: parsedIdToken.sub,
scope: token.scope,
account: {
id: parsedIdToken.sub || 'unknown',
email: parsedIdToken.email || 'unknown',
nickname: parsedIdToken.nickname || 'unknown',
picture: parsedIdToken.picture || 'unknown',
},
};
}
export function IToken2Session(token: IToken): AuthenticationSession {
return {
accessToken: token.accessToken,
account: {
id: token.account.id,
label: token.account.nickname,
picture: token.account.picture,
email: token.account.email,
},
id: token.account.id,
scopes: token.scope.split(' '),
};
return {
accessToken: token.accessToken,
account: {
id: token.account.id,
label: token.account.nickname,
picture: token.account.picture,
email: token.account.email,
},
id: token.account.id,
scopes: token.scope.split(' '),
};
}
export function getRandomValues(input: Uint8Array): Uint8Array {
const bytes = randomBytes(input.length);
for (let i = 0, n = bytes.length; i < n; ++i) {
input[i] = bytes[i];
}
return input;
const bytes = randomBytes(input.length);
for (let i = 0, n = bytes.length; i < n; ++i) {
input[i] = bytes[i];
}
return input;
}
export function generateProofKeyPair() {
const urlEncode = (str: string) =>
str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
const decode = (buffer: Uint8Array | number[]) => {
let decodedString = '';
for (let i = 0; i < buffer.length; i++) {
decodedString += String.fromCharCode(buffer[i]);
}
return decodedString;
};
const buffer = getRandomValues(new Uint8Array(32));
const seed = btoa(decode(buffer));
const urlEncode = (str: string) =>
str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
const decode = (buffer: Uint8Array | number[]) => {
let decodedString = '';
for (let i = 0; i < buffer.length; i++) {
decodedString += String.fromCharCode(buffer[i]);
}
return decodedString;
};
const buffer = getRandomValues(new Uint8Array(32));
const seed = btoa(decode(buffer));
const verifier = urlEncode(seed);
const challenge = urlEncode(
btoa(decode(sha256().update(verifier).digest()))
);
return { verifier, challenge };
const verifier = urlEncode(seed);
const challenge = urlEncode(btoa(decode(sha256().update(verifier).digest())));
return { verifier, challenge };
}

View File

@@ -4,15 +4,15 @@ 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,
BoardListWatchRequest,
BoardListWatchResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
import {
Board,
Port,
NotificationServiceServer,
AvailablePorts,
AttachedBoardsChangeEvent,
Board,
Port,
NotificationServiceServer,
AvailablePorts,
AttachedBoardsChangeEvent,
} from '../common/protocol';
/**
@@ -22,136 +22,130 @@ import {
*/
@injectable()
export class BoardDiscovery extends CoreClientAware {
@inject(ILogger)
@named('discovery')
protected discoveryLogger: ILogger;
@inject(ILogger)
@named('discovery')
protected discoveryLogger: ILogger;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
@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. \
* The `protocol` is ignored because the board detach event does not carry the protocol information,
* just the address.
* ```json
* {
* "type": "remove",
* "address": "/dev/cu.usbmodem14101"
* }
* ```
*/
protected _state: AvailablePorts = {};
get state(): AvailablePorts {
return this._state;
}
/**
* Keys are the `address` of the ports. \
* The `protocol` is ignored because the board detach event does not carry the protocol information,
* just the address.
* ```json
* {
* "type": "remove",
* "address": "/dev/cu.usbmodem14101"
* }
* ```
*/
protected _state: AvailablePorts = {};
get state(): AvailablePorts {
return this._state;
}
@postConstruct()
protected async init(): Promise<void> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new BoardListWatchRequest();
req.setInstance(instance);
this.boardWatchDuplex = client.boardListWatch();
this.boardWatchDuplex.on('data', (resp: BoardListWatchResponse) => {
const detectedPort = resp.getPort();
if (detectedPort) {
let eventType: 'add' | 'remove' | 'unknown' = 'unknown';
if (resp.getEventType() === 'add') {
eventType = 'add';
} else if (resp.getEventType() === 'remove') {
eventType = 'remove';
} else {
eventType = 'unknown';
}
if (eventType === 'unknown') {
throw new Error(
`Unexpected event type: '${resp.getEventType()}'`
);
}
const oldState = deepClone(this._state);
const newState = deepClone(this._state);
const address = detectedPort.getAddress();
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,
});
}
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
)}`
);
}
newState[port.address] = [port, boards];
} else if (eventType === 'remove') {
if (newState[port.address] === undefined) {
console.warn(
`Port '${port.address}' was not available. Skipping`
);
return;
}
delete newState[port.address];
}
const oldAvailablePorts = this.getAvailablePorts(oldState);
const oldAttachedBoards = this.getAttachedBoards(oldState);
const newAvailablePorts = this.getAvailablePorts(newState);
const newAttachedBoards = this.getAttachedBoards(newState);
const event: AttachedBoardsChangeEvent = {
oldState: {
ports: oldAvailablePorts,
boards: oldAttachedBoards,
},
newState: {
ports: newAvailablePorts,
boards: newAttachedBoards,
},
};
this._state = newState;
this.notificationService.notifyAttachedBoardsChanged(event);
}
});
this.boardWatchDuplex.write(req);
}
getAttachedBoards(state: AvailablePorts = this.state): Board[] {
const attachedBoards: Board[] = [];
for (const address of Object.keys(state)) {
const [, boards] = state[address];
attachedBoards.push(...boards);
@postConstruct()
protected async init(): Promise<void> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new BoardListWatchRequest();
req.setInstance(instance);
this.boardWatchDuplex = client.boardListWatch();
this.boardWatchDuplex.on('data', (resp: BoardListWatchResponse) => {
const detectedPort = resp.getPort();
if (detectedPort) {
let eventType: 'add' | 'remove' | 'unknown' = 'unknown';
if (resp.getEventType() === 'add') {
eventType = 'add';
} else if (resp.getEventType() === 'remove') {
eventType = 'remove';
} else {
eventType = 'unknown';
}
return attachedBoards;
}
getAvailablePorts(state: AvailablePorts = this.state): Port[] {
const availablePorts: Port[] = [];
for (const address of Object.keys(state)) {
// tslint:disable-next-line: whitespace
const [port] = state[address];
availablePorts.push(port);
if (eventType === 'unknown') {
throw new Error(`Unexpected event type: '${resp.getEventType()}'`);
}
return availablePorts;
const oldState = deepClone(this._state);
const newState = deepClone(this._state);
const address = detectedPort.getAddress();
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,
});
}
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
)}`
);
}
newState[port.address] = [port, boards];
} else if (eventType === 'remove') {
if (newState[port.address] === undefined) {
console.warn(`Port '${port.address}' was not available. Skipping`);
return;
}
delete newState[port.address];
}
const oldAvailablePorts = this.getAvailablePorts(oldState);
const oldAttachedBoards = this.getAttachedBoards(oldState);
const newAvailablePorts = this.getAvailablePorts(newState);
const newAttachedBoards = this.getAttachedBoards(newState);
const event: AttachedBoardsChangeEvent = {
oldState: {
ports: oldAvailablePorts,
boards: oldAttachedBoards,
},
newState: {
ports: newAvailablePorts,
boards: newAttachedBoards,
},
};
this._state = newState;
this.notificationService.notifyAttachedBoardsChanged(event);
}
});
this.boardWatchDuplex.write(req);
}
getAttachedBoards(state: AvailablePorts = this.state): Board[] {
const attachedBoards: Board[] = [];
for (const address of Object.keys(state)) {
const [, boards] = state[address];
attachedBoards.push(...boards);
}
return attachedBoards;
}
getAvailablePorts(state: AvailablePorts = this.state): Port[] {
const availablePorts: Port[] = [];
for (const address of Object.keys(state)) {
// tslint:disable-next-line: whitespace
const [port] = state[address];
availablePorts.push(port);
}
return availablePorts;
}
}

View File

@@ -2,448 +2,435 @@ import { injectable, inject, named } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { notEmpty } from '@theia/core/lib/common/objects';
import {
BoardsService,
Installable,
BoardsPackage,
Board,
Port,
BoardDetails,
Tool,
ConfigOption,
ConfigValue,
Programmer,
ResponseService,
NotificationServiceServer,
AvailablePorts,
BoardWithPackage,
BoardsService,
Installable,
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,
BoardDetailsRequest,
BoardDetailsResponse,
BoardSearchRequest,
} from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
import {
ListProgrammersAvailableForUploadRequest,
ListProgrammersAvailableForUploadResponse,
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
extends CoreClientAware
implements BoardsService
{
@inject(ILogger)
protected logger: ILogger;
@inject(ILogger)
protected logger: ILogger;
@inject(ILogger)
@named('discovery')
protected discoveryLogger: ILogger;
@inject(ILogger)
@named('discovery')
protected discoveryLogger: ILogger;
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
@inject(BoardDiscovery)
protected readonly boardDiscovery: BoardDiscovery;
@inject(BoardDiscovery)
protected readonly boardDiscovery: BoardDiscovery;
async getState(): Promise<AvailablePorts> {
return this.boardDiscovery.state;
}
async getState(): Promise<AvailablePorts> {
return this.boardDiscovery.state;
}
async getAttachedBoards(): Promise<Board[]> {
return this.boardDiscovery.getAttachedBoards();
}
async getAttachedBoards(): Promise<Board[]> {
return this.boardDiscovery.getAttachedBoards();
}
async getAvailablePorts(): Promise<Port[]> {
return this.boardDiscovery.getAvailablePorts();
}
async getAvailablePorts(): Promise<Port[]> {
return this.boardDiscovery.getAvailablePorts();
}
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);
})
);
if (!detailsResp) {
return undefined;
}
const debuggingSupported = detailsResp.getDebuggingSupported();
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 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 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);
if (usbId) {
VID = usbId.getVid();
PID = usbId.getPid();
}
return {
fqbn,
requiredTools,
configOptions,
programmers,
debuggingSupported,
VID,
PID,
};
}
async getBoardPackage(options: {
id: string;
}): Promise<BoardsPackage | undefined> {
const { id: expectedId } = options;
if (!expectedId) {
return undefined;
}
const packages = await this.search({ query: expectedId });
return packages.find(({ id }) => id === expectedId);
}
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)
);
}
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(),
});
}
}
resolve(boards);
});
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;
}
);
return boards;
// 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;
}
async search(options: { query?: string }): Promise<BoardsPackage[]> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const debuggingSupported = detailsResp.getDebuggingSupported();
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 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 packages = new Map<string, BoardsPackage>();
const toPackage = (platform: Platform) => {
let installedVersion: string | undefined;
const matchingPlatform = installedPlatforms.find(
(ip) => ip.getId() === platform.getId()
);
if (!!matchingPlatform) {
installedVersion = matchingPlatform.getInstalled();
}
return {
id: platform.getId(),
name: platform.getName(),
author: platform.getMaintainer(),
availableVersions: [platform.getLatest()],
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(),
};
};
// 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.
const groupedById: Map<string, Platform[]> = new Map();
for (const platform of resp.getSearchOutputList()) {
const id = platform.getId();
if (groupedById.has(id)) {
groupedById.get(id)!.push(platform);
} else {
groupedById.set(id, [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()
);
if (leftInstalled && !rightInstalled) {
return -1;
}
if (!leftInstalled && rightInstalled) {
return 1;
}
return Installable.Version.COMPARATOR(
left.getLatest(),
right.getLatest()
); // Higher version comes first.
};
for (const id of groupedById.keys()) {
groupedById.get(id)!.sort(installedAwareVersionComparator);
const requiredTools = detailsResp.getToolsDependenciesList().map(
(t) =>
<Tool>{
name: t.getName(),
packager: t.getPackager(),
version: t.getVersion(),
}
);
for (const id of groupedById.keys()) {
for (const platform of groupedById.get(id)!) {
const id = platform.getId();
const pkg = packages.get(id);
if (pkg) {
pkg.availableVersions.push(platform.getLatest());
pkg.availableVersions
.sort(Installable.Version.COMPARATOR)
.reverse();
} else {
packages.set(id, toPackage(platform));
}
}
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(),
}
),
}
);
return [...packages.values()];
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 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);
if (usbId) {
VID = usbId.getVid();
PID = usbId.getPid();
}
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 coreClient = await this.coreClient();
const { client, instance } = coreClient;
return {
fqbn,
requiredTools,
configOptions,
programmers,
debuggingSupported,
VID,
PID,
};
}
const [platform, architecture] = item.id.split(':');
async getBoardPackage(options: {
id: string;
}): Promise<BoardsPackage | undefined> {
const { id: expectedId } = options;
if (!expectedId) {
return undefined;
}
const packages = await this.search({ query: expectedId });
return packages.find(({ id }) => id === expectedId);
}
const req = new PlatformInstallRequest();
req.setInstance(instance);
req.setArchitecture(architecture);
req.setPlatformPackage(platform);
req.setVersion(version);
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)
);
}
console.info('>>> Starting boards package installation...', item);
const resp = client.platformInstall(req);
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(),
});
reject(error);
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(),
});
});
}
}
resolve(boards);
});
});
return boards;
}
const items = await this.search({});
const updated =
items.find((other) => BoardsPackage.equals(other, item)) || item;
this.notificationService.notifyPlatformInstalled({ item: updated });
console.info('<<< Boards package installation done.', item);
async search(options: { query?: string }): Promise<BoardsPackage[]> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
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 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 packages = new Map<string, BoardsPackage>();
const toPackage = (platform: Platform) => {
let installedVersion: string | undefined;
const matchingPlatform = installedPlatforms.find(
(ip) => ip.getId() === platform.getId()
);
if (!!matchingPlatform) {
installedVersion = matchingPlatform.getInstalled();
}
return {
id: platform.getId(),
name: platform.getName(),
author: platform.getMaintainer(),
availableVersions: [platform.getLatest()],
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(),
};
};
// 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.
const groupedById: Map<string, Platform[]> = new Map();
for (const platform of resp.getSearchOutputList()) {
const id = platform.getId();
if (groupedById.has(id)) {
groupedById.get(id)!.push(platform);
} else {
groupedById.set(id, [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()
);
if (leftInstalled && !rightInstalled) {
return -1;
}
if (!leftInstalled && rightInstalled) {
return 1;
}
return Installable.Version.COMPARATOR(
left.getLatest(),
right.getLatest()
); // Higher version comes first.
};
for (const id of groupedById.keys()) {
groupedById.get(id)!.sort(installedAwareVersionComparator);
}
async uninstall(options: {
item: BoardsPackage;
progressId?: string;
}): Promise<void> {
const { item, progressId } = options;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const [platform, architecture] = item.id.split(':');
const req = new PlatformUninstallRequest();
req.setInstance(instance);
req.setArchitecture(architecture);
req.setPlatformPackage(platform);
console.info('>>> Starting boards package uninstallation...', item);
const resp = client.platformUninstall(req);
resp.on(
'data',
InstallWithProgress.createDataCallback({
progressId,
responseService: this.responseService,
})
);
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);
});
// Here, unlike at `install` we send out the argument `item`. Otherwise, we would not know about the board FQBN.
this.notificationService.notifyPlatformUninstalled({ item });
console.info('<<< Boards package uninstallation done.', item);
for (const id of groupedById.keys()) {
for (const platform of groupedById.get(id)!) {
const id = platform.getId();
const pkg = packages.get(id);
if (pkg) {
pkg.availableVersions.push(platform.getLatest());
pkg.availableVersions.sort(Installable.Version.COMPARATOR).reverse();
} else {
packages.set(id, toPackage(platform));
}
}
}
return [...packages.values()];
}
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 coreClient = await this.coreClient();
const { client, instance } = coreClient;
const [platform, architecture] = item.id.split(':');
const req = new PlatformInstallRequest();
req.setInstance(instance);
req.setArchitecture(architecture);
req.setPlatformPackage(platform);
req.setVersion(version);
console.info('>>> Starting boards package installation...', item);
const resp = client.platformInstall(req);
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(),
});
reject(error);
});
});
const items = await this.search({});
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> {
const { item, progressId } = options;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const [platform, architecture] = item.id.split(':');
const req = new PlatformUninstallRequest();
req.setInstance(instance);
req.setArchitecture(architecture);
req.setPlatformPackage(platform);
console.info('>>> Starting boards package uninstallation...', item);
const resp = client.platformUninstall(req);
resp.on(
'data',
InstallWithProgress.createDataCallback({
progressId,
responseService: this.responseService,
})
);
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);
});
// Here, unlike at `install` we send out the argument `item`. Otherwise, we would not know about the board FQBN.
this.notificationService.notifyPlatformUninstalled({ item });
console.info('<<< Boards package uninstallation done.', item);
}
}

View File

@@ -3,160 +3,154 @@ import { RecursivePartial } from '@theia/core/lib/common/types';
export const CLI_CONFIG = 'arduino-cli.yaml';
export interface BoardManager {
readonly additional_urls: Array<string>;
readonly additional_urls: Array<string>;
}
export namespace BoardManager {
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 || [])
);
if (leftUrls.length !== rightUrls.length) {
return false;
}
return leftUrls.every((url) => rightUrls.indexOf(url) !== -1);
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 || []));
if (leftUrls.length !== rightUrls.length) {
return false;
}
return leftUrls.every((url) => rightUrls.indexOf(url) !== -1);
}
}
export interface Daemon {
readonly port: string | number;
readonly port: string | number;
}
export namespace Daemon {
export function is(
daemon: RecursivePartial<Daemon> | undefined
): daemon is Daemon {
return !!daemon && !!daemon.port;
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 {
if (left === undefined) {
return right === undefined;
}
export function sameAs(
left: RecursivePartial<Daemon> | undefined,
right: RecursivePartial<Daemon> | undefined
): boolean {
if (left === undefined) {
return right === undefined;
}
if (right === undefined) {
return left === undefined;
}
return String(left.port) === String(right.port);
if (right === undefined) {
return left === undefined;
}
return String(left.port) === String(right.port);
}
}
export interface Directories {
readonly data: string;
readonly downloads: string;
readonly user: string;
readonly data: string;
readonly downloads: string;
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 {
if (left === undefined) {
return right === undefined;
}
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
);
if (right === undefined) {
return left === undefined;
}
return (
left.data === right.data &&
left.downloads === right.downloads &&
left.user === right.user
);
}
}
export interface Logging {
file: string;
format: Logging.Format;
level: Logging.Level;
file: string;
format: Logging.Format;
level: Logging.Level;
}
export namespace Logging {
export type Format = 'text' | 'json';
export type Level =
| 'trace'
| 'debug'
| 'info'
| 'warning'
| 'error'
| 'fatal'
| 'panic';
export type Format = 'text' | 'json';
export type Level =
| 'trace'
| 'debug'
| 'info'
| 'warning'
| 'error'
| 'fatal'
| 'panic';
export function sameAs(
left: RecursivePartial<Logging> | undefined,
right: RecursivePartial<Logging> | undefined
): boolean {
if (left === undefined) {
return right === undefined;
}
if (right === undefined) {
return left === undefined;
}
if (left.file !== right.file) {
return false;
}
if (left.format !== right.format) {
return false;
}
if (left.level !== right.level) {
return false;
}
return true;
export function sameAs(
left: RecursivePartial<Logging> | undefined,
right: RecursivePartial<Logging> | undefined
): boolean {
if (left === undefined) {
return right === undefined;
}
if (right === undefined) {
return left === undefined;
}
if (left.file !== right.file) {
return false;
}
if (left.format !== right.format) {
return false;
}
if (left.level !== right.level) {
return false;
}
return true;
}
}
export interface Network {
proxy?: string;
proxy?: string;
}
// Arduino CLI config scheme
export interface CliConfig {
board_manager?: RecursivePartial<BoardManager>;
directories?: RecursivePartial<Directories>;
logging?: RecursivePartial<Logging>;
network?: RecursivePartial<Network>;
board_manager?: RecursivePartial<BoardManager>;
directories?: RecursivePartial<Directories>;
logging?: RecursivePartial<Logging>;
network?: RecursivePartial<Network>;
}
// Bare minimum required CLI config.
export interface DefaultCliConfig extends CliConfig {
directories: Directories;
daemon: Daemon;
directories: Directories;
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 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 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)
);
}
}

View File

@@ -11,15 +11,15 @@ 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,
ConfigService,
Config,
NotificationServiceServer,
Network,
} from '../common/protocol';
import { spawnCommand } from './exec-util';
import {
MergeRequest,
WriteRequest,
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';
@@ -34,273 +34,268 @@ const track = temp.track();
@injectable()
export class ConfigServiceImpl
implements BackendApplicationContribution, ConfigService
implements BackendApplicationContribution, ConfigService
{
@inject(ILogger)
@named('config')
protected readonly logger: ILogger;
@inject(ILogger)
@named('config')
protected readonly logger: ILogger;
@inject(EnvVariablesServer)
protected readonly envVariablesServer: EnvVariablesServer;
@inject(EnvVariablesServer)
protected readonly envVariablesServer: EnvVariablesServer;
@inject(ArduinoDaemonImpl)
protected readonly daemon: ArduinoDaemonImpl;
@inject(ArduinoDaemonImpl)
protected readonly daemon: ArduinoDaemonImpl;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
protected config: Config;
protected cliConfig: DefaultCliConfig | undefined;
protected ready = new Deferred<void>();
protected readonly configChangeEmitter = new Emitter<Config>();
protected config: Config;
protected cliConfig: DefaultCliConfig | undefined;
protected ready = new Deferred<void>();
protected readonly configChangeEmitter = new Emitter<Config>();
async onStart(): Promise<void> {
await this.ensureCliConfigExists();
this.cliConfig = await this.loadCliConfig();
if (this.cliConfig) {
const config = await this.mapCliConfigToAppConfig(this.cliConfig);
if (config) {
this.config = config;
this.ready.resolve();
return;
}
async onStart(): Promise<void> {
await this.ensureCliConfigExists();
this.cliConfig = await this.loadCliConfig();
if (this.cliConfig) {
const config = await this.mapCliConfigToAppConfig(this.cliConfig);
if (config) {
this.config = config;
this.ready.resolve();
return;
}
}
this.fireInvalidConfig();
}
async getCliConfigFileUri(): Promise<string> {
const configDirUri = await this.envVariablesServer.getConfigDirUri();
return new URI(configDirUri).resolve(CLI_CONFIG).toString();
}
async getConfiguration(): Promise<Config> {
await this.ready.promise;
return this.config;
}
async setConfiguration(config: Config): Promise<void> {
await this.ready.promise;
if (Config.sameAs(this.config, config)) {
return;
}
let copyDefaultCliConfig: DefaultCliConfig | undefined = deepClone(
this.cliConfig
);
if (!copyDefaultCliConfig) {
copyDefaultCliConfig = await this.getFallbackCliConfig();
}
const {
additionalUrls,
dataDirUri,
downloadsDirUri,
sketchDirUri,
network,
} = config;
copyDefaultCliConfig.directories = {
data: FileUri.fsPath(dataDirUri),
downloads: FileUri.fsPath(downloadsDirUri),
user: FileUri.fsPath(sketchDirUri),
};
copyDefaultCliConfig.board_manager = {
additional_urls: [...additionalUrls],
};
const proxy = Network.stringify(network);
copyDefaultCliConfig.network = { proxy };
const { port } = copyDefaultCliConfig.daemon;
await this.updateDaemon(port, copyDefaultCliConfig);
await this.writeDaemonState(port);
this.config = deepClone(config);
this.cliConfig = copyDefaultCliConfig;
this.fireConfigChanged(this.config);
}
get cliConfiguration(): DefaultCliConfig | undefined {
return this.cliConfig;
}
get onConfigChange(): Event<Config> {
return this.configChangeEmitter.event;
}
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))
);
}
async isInSketchDir(uri: string): Promise<boolean> {
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 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
);
}
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;
}
this.fireInvalidConfig();
}
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;
}
async getCliConfigFileUri(): Promise<string> {
const configDirUri = await this.envVariablesServer.getConfigDirUri();
return new URI(configDirUri).resolve(CLI_CONFIG).toString();
}
async getConfiguration(): Promise<Config> {
await this.ready.promise;
return this.config;
}
async setConfiguration(config: Config): Promise<void> {
await this.ready.promise;
if (Config.sameAs(this.config, config)) {
return;
}
let copyDefaultCliConfig: DefaultCliConfig | undefined = deepClone(
this.cliConfig
protected async ensureCliConfigExists(): Promise<void> {
const cliConfigFileUri = await this.getCliConfigFileUri();
const cliConfigPath = FileUri.fsPath(cliConfigFileUri);
let exists = await promisify(fs.exists)(cliConfigPath);
if (!exists) {
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}.`
);
if (!copyDefaultCliConfig) {
copyDefaultCliConfig = await this.getFallbackCliConfig();
}
const {
additionalUrls,
dataDirUri,
downloadsDirUri,
sketchDirUri,
network,
} = config;
copyDefaultCliConfig.directories = {
data: FileUri.fsPath(dataDirUri),
downloads: FileUri.fsPath(downloadsDirUri),
user: FileUri.fsPath(sketchDirUri),
};
copyDefaultCliConfig.board_manager = {
additional_urls: [...additionalUrls],
};
const proxy = Network.stringify(network);
copyDefaultCliConfig.network = { proxy };
const { port } = copyDefaultCliConfig.daemon;
await this.updateDaemon(port, copyDefaultCliConfig);
await this.writeDaemonState(port);
this.config = deepClone(config);
this.cliConfig = copyDefaultCliConfig;
this.fireConfigChanged(this.config);
}
}
}
get cliConfiguration(): DefaultCliConfig | undefined {
return this.cliConfig;
protected async initCliConfigTo(fsPathToDir: string): Promise<void> {
const cliPath = await this.daemon.getExecPath();
await spawnCommand(`"${cliPath}"`, [
'config',
'init',
'--dest-dir',
`"${fsPathToDir}"`,
]);
}
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))
);
}
const network = Network.parse(cliConfig.network?.proxy);
return {
dataDirUri: FileUri.create(data).toString(),
sketchDirUri: FileUri.create(user).toString(),
downloadsDirUri: FileUri.create(downloads).toString(),
additionalUrls,
network,
};
}
get onConfigChange(): Event<Config> {
return this.configChangeEmitter.event;
}
protected fireConfigChanged(config: Config): void {
this.configChangeEmitter.fire(config);
this.notificationService.notifyConfigChanged({ config });
}
async getVersion(): Promise<
Readonly<{ version: string; commit: string; status?: string }>
> {
return this.daemon.getVersion();
}
protected fireInvalidConfig(): void {
this.notificationService.notifyConfigChanged({ config: undefined });
}
async isInDataDir(uri: string): Promise<boolean> {
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))
);
}
protected async loadCliConfig(): Promise<DefaultCliConfig | undefined> {
const cliConfigFileUri = await this.getCliConfigFileUri();
const cliConfigPath = FileUri.fsPath(cliConfigFileUri);
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) => {
try {
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
);
if (error) {
reject(error);
return;
}
resolve();
} finally {
client.close();
}
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 model = yaml.safeLoad(rawYaml.trim());
return model as DefaultCliConfig;
}
protected async ensureCliConfigExists(): Promise<void> {
const cliConfigFileUri = await this.getCliConfigFileUri();
const cliConfigPath = FileUri.fsPath(cliConfigFileUri);
let exists = await promisify(fs.exists)(cliConfigPath);
if (!exists) {
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}.`
);
}
protected async writeDaemonState(port: string | number): Promise<void> {
const client = this.createClient(port);
const req = new WriteRequest();
const cliConfigUri = await this.getCliConfigFileUri();
const cliConfigPath = FileUri.fsPath(cliConfigUri);
req.setFilePath(cliConfigPath);
return new Promise<void>((resolve, reject) => {
client.write(req, (error) => {
try {
if (error) {
reject(error);
return;
}
resolve();
} finally {
client.close();
}
}
});
});
}
protected async initCliConfigTo(fsPathToDir: string): Promise<void> {
const cliPath = await this.daemon.getExecPath();
await spawnCommand(`"${cliPath}"`, [
'config',
'init',
'--dest-dir',
`"${fsPathToDir}"`,
]);
}
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))
);
}
const network = Network.parse(cliConfig.network?.proxy);
return {
dataDirUri: FileUri.create(data).toString(),
sketchDirUri: FileUri.create(user).toString(),
downloadsDirUri: FileUri.create(downloads).toString(),
additionalUrls,
network,
};
}
protected fireConfigChanged(config: Config): void {
this.configChangeEmitter.fire(config);
this.notificationService.notifyConfigChanged({ config });
}
protected fireInvalidConfig(): void {
this.notificationService.notifyConfigChanged({ config: undefined });
}
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) => {
try {
if (error) {
reject(error);
return;
}
resolve();
} finally {
client.close();
}
});
});
}
protected async writeDaemonState(port: string | number): Promise<void> {
const client = this.createClient(port);
const req = new WriteRequest();
const cliConfigUri = await this.getCliConfigFileUri();
const cliConfigPath = FileUri.fsPath(cliConfigUri);
req.setFilePath(cliConfigPath);
return new Promise<void>((resolve, reject) => {
client.write(req, (error) => {
try {
if (error) {
reject(error);
return;
}
resolve();
} finally {
client.close();
}
});
});
}
private createClient(port: string | number): SettingsServiceClient {
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
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;
}
private createClient(port: string | number): SettingsServiceClient {
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
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

@@ -6,235 +6,228 @@ 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,
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;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
protected readonly onClientReadyEmitter = new Emitter<void>();
protected readonly onClientReadyEmitter = new Emitter<void>();
get onClientReady(): Event<void> {
return this.onClientReadyEmitter.event;
get onClientReady(): Event<void> {
return this.onClientReadyEmitter.event;
}
close(client: CoreClientProvider.Client): void {
client.client.close();
}
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)) {
await this.updateIndexes(this._client);
this.onClientReadyEmitter.fire();
}
} else {
await super.reconcileClient(port);
this.onClientReadyEmitter.fire();
}
}
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
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('end', () => resolve(resp!));
stream.on('error', (err) => reject(err));
});
const instance = initResp.getInstance();
if (!instance) {
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> {
// in a separate promise, try and update the index
let indexUpdateSucceeded = true;
for (let i = 0; i < 10; i++) {
try {
await this.updateIndex({ client, instance });
indexUpdateSucceeded = true;
break;
} catch (e) {
console.error(`Error while updating index in attempt ${i}.`, e);
}
}
if (!indexUpdateSucceeded) {
console.error('Could not update the index. Please restart to try again.');
}
close(client: CoreClientProvider.Client): void {
client.client.close();
let libIndexUpdateSucceeded = true;
for (let i = 0; i < 10; i++) {
try {
await this.updateLibraryIndex({ client, instance });
libIndexUpdateSucceeded = true;
break;
} catch (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.'
);
}
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)) {
await this.updateIndexes(this._client);
this.onClientReadyEmitter.fire();
if (indexUpdateSucceeded && libIndexUpdateSucceeded) {
this.notificationService.notifyIndexUpdated();
}
}
protected async updateLibraryIndex({
client,
instance,
}: CoreClientProvider.Client): Promise<void> {
const req = new UpdateLibrariesIndexRequest();
req.setInstance(instance);
const resp = client.updateLibrariesIndex(req);
let file: string | undefined;
resp.on('data', (data: UpdateLibrariesIndexResponse) => {
const progress = data.getDownloadProgress();
if (progress) {
if (!file && progress.getFile()) {
file = `${progress.getFile()}`;
}
if (progress.getCompleted()) {
if (file) {
if (/\s/.test(file)) {
console.log(`${file} completed.`);
} else {
console.log(`Download of '${file}' completed.`);
}
} else {
await super.reconcileClient(port);
this.onClientReadyEmitter.fire();
} else {
console.log('The library index has been successfully updated.');
}
file = undefined;
}
}
}
});
await new Promise<void>((resolve, reject) => {
resp.on('error', reject);
resp.on('end', resolve);
});
}
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
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('end', () => resolve(resp!));
stream.on('error', (err) => reject(err));
});
const instance = initResp.getInstance();
if (!instance) {
throw new Error(
'Could not retrieve instance from the initialize response.'
);
protected async updateIndex({
client,
instance,
}: CoreClientProvider.Client): Promise<void> {
const updateReq = new UpdateIndexRequest();
updateReq.setInstance(instance);
const updateResp = client.updateIndex(updateReq);
let file: string | undefined;
updateResp.on('data', (o: UpdateIndexResponse) => {
const progress = o.getDownloadProgress();
if (progress) {
if (!file && progress.getFile()) {
file = `${progress.getFile()}`;
}
await this.updateIndexes({ instance, client });
return { instance, client };
}
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++) {
try {
await this.updateIndex({ client, instance });
indexUpdateSucceeded = true;
break;
} catch (e) {
console.error(`Error while updating index in attempt ${i}.`, e);
if (progress.getCompleted()) {
if (file) {
if (/\s/.test(file)) {
console.log(`${file} completed.`);
} else {
console.log(`Download of '${file}' completed.`);
}
} else {
console.log('The index has been successfully updated.');
}
file = undefined;
}
if (!indexUpdateSucceeded) {
console.error(
'Could not update the index. Please restart to try again.'
);
}
let libIndexUpdateSucceeded = true;
for (let i = 0; i < 10; i++) {
try {
await this.updateLibraryIndex({ client, instance });
libIndexUpdateSucceeded = true;
break;
} catch (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.'
);
}
if (indexUpdateSucceeded && libIndexUpdateSucceeded) {
this.notificationService.notifyIndexUpdated();
}
}
protected async updateLibraryIndex({
client,
instance,
}: CoreClientProvider.Client): Promise<void> {
const req = new UpdateLibrariesIndexRequest();
req.setInstance(instance);
const resp = client.updateLibrariesIndex(req);
let file: string | undefined;
resp.on('data', (data: UpdateLibrariesIndexResponse) => {
const progress = data.getDownloadProgress();
if (progress) {
if (!file && progress.getFile()) {
file = `${progress.getFile()}`;
}
if (progress.getCompleted()) {
if (file) {
if (/\s/.test(file)) {
console.log(`${file} completed.`);
} else {
console.log(`Download of '${file}' completed.`);
}
} else {
console.log(
'The library index has been successfully updated.'
);
}
file = undefined;
}
}
});
await new Promise<void>((resolve, reject) => {
resp.on('error', reject);
resp.on('end', resolve);
});
}
protected async updateIndex({
client,
instance,
}: CoreClientProvider.Client): Promise<void> {
const updateReq = new UpdateIndexRequest();
updateReq.setInstance(instance);
const updateResp = client.updateIndex(updateReq);
let file: string | undefined;
updateResp.on('data', (o: UpdateIndexResponse) => {
const progress = o.getDownloadProgress();
if (progress) {
if (!file && progress.getFile()) {
file = `${progress.getFile()}`;
}
if (progress.getCompleted()) {
if (file) {
if (/\s/.test(file)) {
console.log(`${file} completed.`);
} else {
console.log(`Download of '${file}' completed.`);
}
} else {
console.log('The index has been successfully updated.');
}
file = undefined;
}
}
});
await new Promise<void>((resolve, reject) => {
updateResp.on('error', reject);
updateResp.on('end', resolve);
});
}
}
});
await new Promise<void>((resolve, reject) => {
updateResp.on('error', reject);
updateResp.on('end', resolve);
});
}
}
export namespace CoreClientProvider {
export interface Client {
readonly client: ArduinoCoreServiceClient;
readonly instance: Instance;
}
export interface Client {
readonly client: ArduinoCoreServiceClient;
readonly instance: Instance;
}
}
@injectable()
export abstract class CoreClientAware {
@inject(CoreClientProvider)
protected readonly coreClientProvider: CoreClientProvider;
@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 client = await this.coreClientProvider.client();
if (client) {
handle(client);
}
toDispose.dispose();
})
);
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 client = await this.coreClientProvider.client();
if (client) {
handle(client);
}
toDispose.dispose();
})
);
return coreClient;
}
}
);
return coreClient;
}
}

View File

@@ -6,17 +6,17 @@ 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,
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,
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';
@@ -25,205 +25,201 @@ import { firstToUpperCase, firstToLowerCase } from '../common/utils';
@injectable()
export class CoreServiceImpl extends CoreClientAware implements CoreService {
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
async compile(
options: CoreService.Compile.Options & {
exportBinaries?: boolean;
compilerWarnings?: CompilerWarnings;
}
): Promise<void> {
const { sketchUri, fqbn, compilerWarnings } = options;
const sketchPath = FileUri.fsPath(sketchUri);
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const compileReq = new CompileRequest();
compileReq.setInstance(instance);
compileReq.setSketchPath(sketchPath);
if (fqbn) {
compileReq.setFqbn(fqbn);
}
if (compilerWarnings) {
compileReq.setWarnings(compilerWarnings.toLowerCase());
}
compileReq.setOptimizeForDebug(options.optimizeForDebug);
compileReq.setPreprocess(false);
compileReq.setVerbose(options.verbose);
compileReq.setQuiet(false);
if (typeof options.exportBinaries === 'boolean') {
const exportBinaries = new BoolValue();
exportBinaries.setValue(options.exportBinaries);
compileReq.setExportBinaries(exportBinaries);
}
this.mergeSourceOverrides(compileReq, options);
const result = client.compile(compileReq);
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(),
});
});
result.on('error', (error) => reject(error));
result.on('end', () => resolve());
});
this.responseService.appendToOutput({
chunk: '\n--------------------------\nCompilation complete.\n',
});
} catch (e) {
this.responseService.appendToOutput({
chunk: `Compilation error: ${e}\n`,
severity: 'error',
});
throw e;
}
async compile(
options: CoreService.Compile.Options & {
exportBinaries?: boolean;
compilerWarnings?: CompilerWarnings;
}
): Promise<void> {
const { sketchUri, fqbn, compilerWarnings } = options;
const sketchPath = FileUri.fsPath(sketchUri);
async upload(options: CoreService.Upload.Options): Promise<void> {
await this.doUpload(
options,
() => new UploadRequest(),
(client, req) => client.upload(req)
);
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const compileReq = new CompileRequest();
compileReq.setInstance(instance);
compileReq.setSketchPath(sketchPath);
if (fqbn) {
compileReq.setFqbn(fqbn);
}
async uploadUsingProgrammer(
options: CoreService.Upload.Options
): Promise<void> {
await this.doUpload(
options,
() => new UploadUsingProgrammerRequest(),
(client, req) => client.uploadUsingProgrammer(req),
'upload using programmer'
);
if (compilerWarnings) {
compileReq.setWarnings(compilerWarnings.toLowerCase());
}
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 = 'upload'
): Promise<void> {
await this.compile(Object.assign(options, { exportBinaries: false }));
const { sketchUri, fqbn, port, programmer } = options;
const sketchPath = FileUri.fsPath(sketchUri);
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = requestProvider();
req.setInstance(instance);
req.setSketchPath(sketchPath);
if (fqbn) {
req.setFqbn(fqbn);
}
if (port) {
req.setPort(port);
}
if (programmer) {
req.setProgrammer(programmer.id);
}
req.setVerbose(options.verbose);
req.setVerify(options.verify);
const result = responseHandler(client, req);
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(),
});
});
result.on('error', (error) => reject(error));
result.on('end', () => resolve());
});
this.responseService.appendToOutput({
chunk:
'\n--------------------------\n' +
firstToLowerCase(task) +
' complete.\n',
});
} catch (e) {
this.responseService.appendToOutput({
chunk: `${firstToUpperCase(task)} error: ${e}\n`,
severity: 'error',
});
throw e;
}
compileReq.setOptimizeForDebug(options.optimizeForDebug);
compileReq.setPreprocess(false);
compileReq.setVerbose(options.verbose);
compileReq.setQuiet(false);
if (typeof options.exportBinaries === 'boolean') {
const exportBinaries = new BoolValue();
exportBinaries.setValue(options.exportBinaries);
compileReq.setExportBinaries(exportBinaries);
}
this.mergeSourceOverrides(compileReq, options);
async burnBootloader(
options: CoreService.Bootloader.Options
): Promise<void> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const { fqbn, port, programmer } = options;
const burnReq = new BurnBootloaderRequest();
burnReq.setInstance(instance);
if (fqbn) {
burnReq.setFqbn(fqbn);
}
if (port) {
burnReq.setPort(port);
}
if (programmer) {
burnReq.setProgrammer(programmer.id);
}
burnReq.setVerify(options.verify);
burnReq.setVerbose(options.verbose);
const result = client.burnBootloader(burnReq);
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(),
});
});
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',
});
throw e;
}
const result = client.compile(compileReq);
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(),
});
});
result.on('error', (error) => reject(error));
result.on('end', () => resolve());
});
this.responseService.appendToOutput({
chunk: '\n--------------------------\nCompilation complete.\n',
});
} catch (e) {
this.responseService.appendToOutput({
chunk: `Compilation error: ${e}\n`,
severity: 'error',
});
throw e;
}
}
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];
if (content) {
const relativePath = relative(sketchPath, FileUri.fsPath(uri));
req.getSourceOverrideMap().set(relativePath, content);
}
}
async upload(options: CoreService.Upload.Options): Promise<void> {
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'
);
}
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 = 'upload'
): Promise<void> {
await this.compile(Object.assign(options, { exportBinaries: false }));
const { sketchUri, fqbn, port, programmer } = options;
const sketchPath = FileUri.fsPath(sketchUri);
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = requestProvider();
req.setInstance(instance);
req.setSketchPath(sketchPath);
if (fqbn) {
req.setFqbn(fqbn);
}
if (port) {
req.setPort(port);
}
if (programmer) {
req.setProgrammer(programmer.id);
}
req.setVerbose(options.verbose);
req.setVerify(options.verify);
const result = responseHandler(client, req);
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(),
});
});
result.on('error', (error) => reject(error));
result.on('end', () => resolve());
});
this.responseService.appendToOutput({
chunk:
'\n--------------------------\n' +
firstToLowerCase(task) +
' complete.\n',
});
} catch (e) {
this.responseService.appendToOutput({
chunk: `${firstToUpperCase(task)} error: ${e}\n`,
severity: 'error',
});
throw e;
}
}
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const { fqbn, port, programmer } = options;
const burnReq = new BurnBootloaderRequest();
burnReq.setInstance(instance);
if (fqbn) {
burnReq.setFqbn(fqbn);
}
if (port) {
burnReq.setPort(port);
}
if (programmer) {
burnReq.setProgrammer(programmer.id);
}
burnReq.setVerify(options.verify);
burnReq.setVerbose(options.verbose);
const result = client.burnBootloader(burnReq);
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(),
});
});
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',
});
throw e;
}
}
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];
if (content) {
const relativePath = relative(sketchPath, FileUri.fsPath(uri));
req.getSourceOverrideMap().set(relativePath, content);
}
}
}
}

View File

@@ -1,155 +1,153 @@
import { ILogger, LogLevel } from '@theia/core/lib/common/logger';
export interface DaemonLog {
readonly time: string;
readonly level: DaemonLog.Level;
readonly msg: string;
readonly time: string;
readonly level: DaemonLog.Level;
readonly msg: string;
}
export namespace DaemonLog {
export interface Url {
readonly Scheme: string;
readonly Host: string;
readonly Path: string;
export interface Url {
readonly Scheme: string;
readonly Host: string;
readonly Path: string;
}
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'
);
}
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'
);
}
export function toString(url: Url): string {
const { Scheme, Host, Path } = url;
return `${Scheme}://${Host}${Path}`;
export function toString(url: Url): string {
const { Scheme, Host, Path } = url;
return `${Scheme}://${Host}${Path}`;
}
}
export interface System {
readonly os: string;
// readonly Resource: Resource;
}
export namespace System {
export function toString(system: System): string {
return `OS: ${system.os}`;
}
}
export interface Tool {
readonly version: string;
readonly systems: System[];
}
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(', ')}]`
: ''
}`;
}
}
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'
);
}
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;
}
}
export function log(logger: ILogger, logMessages: string): void {
const parsed = parse(logMessages);
for (const log of parsed) {
const logLevel = toLogLevel(log);
const message = toMessage(log, { omitLogLevel: true });
logger.log(logLevel, message);
}
}
function parse(toLog: string): DaemonLog[] {
const messages = toLog.trim().split('\n');
const result: DaemonLog[] = [];
for (let i = 0; i < messages.length; i++) {
try {
const maybeDaemonLog = JSON.parse(messages[i]);
if (DaemonLog.is(maybeDaemonLog)) {
result.push(maybeDaemonLog);
continue;
}
} catch {
/* NOOP */
}
result.push({
time: new Date().toString(),
level: 'info',
msg: messages[i],
});
}
return result;
}
export interface System {
readonly os: string;
// readonly Resource: Resource;
}
export namespace System {
export function toString(system: System): string {
return `OS: ${system.os}`;
}
}
export interface Tool {
readonly version: string;
readonly systems: System[];
}
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(', ')}]`
: ''
}`;
}
}
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'
);
}
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;
}
}
export function log(logger: ILogger, logMessages: string): void {
const parsed = parse(logMessages);
for (const log of parsed) {
const logLevel = toLogLevel(log);
const message = toMessage(log, { omitLogLevel: true });
logger.log(logLevel, message);
}
}
function parse(toLog: string): DaemonLog[] {
const messages = toLog.trim().split('\n');
const result: DaemonLog[] = [];
for (let i = 0; i < messages.length; i++) {
try {
const maybeDaemonLog = JSON.parse(messages[i]);
if (DaemonLog.is(maybeDaemonLog)) {
result.push(maybeDaemonLog);
continue;
}
} catch {
/* NOOP */
}
result.push({
time: new Date().toString(),
level: 'info',
msg: messages[i],
});
}
return result;
}
export function toPrettyString(logMessages: string): string {
const parsed = parse(logMessages);
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 toDetails(log: DaemonLog, key: string): string {
let value = (log as any)[key];
if (DaemonLog.Url.is(value)) {
value = DaemonLog.Url.toString(value);
} else if (DaemonLog.Tool.is(value)) {
value = DaemonLog.Tool.toString(value);
} else if (typeof value === 'object') {
value = JSON.stringify(value).replace(/\"([^(\")"]+)\":/g, '$1:'); // Remove the quotes from the property keys.
}
return `${key.toLowerCase()}: ${value}`;
export function toPrettyString(logMessages: string): string {
const parsed = parse(logMessages);
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 toDetails(log: DaemonLog, key: string): string {
let value = (log as any)[key];
if (DaemonLog.Url.is(value)) {
value = DaemonLog.Url.toString(value);
} else if (DaemonLog.Tool.is(value)) {
value = DaemonLog.Tool.toString(value);
} else if (typeof value === 'object') {
value = JSON.stringify(value).replace(/\"([^(\")"]+)\":/g, '$1:'); // Remove the quotes from the property keys.
}
return `${key.toLowerCase()}: ${value}`;
}
}

View File

@@ -1,19 +1,19 @@
import * as psTree from 'ps-tree';
const kill = require('tree-kill');
const [theiaPid, daemonPid] = process.argv
.slice(2)
.map((id) => Number.parseInt(id, 10));
.slice(2)
.map((id) => Number.parseInt(id, 10));
setInterval(() => {
try {
// Throws an exception if the Theia process doesn't exist anymore.
process.kill(theiaPid, 0);
} catch {
psTree(daemonPid, function (_, children) {
for (const { PID } of children) {
kill(PID);
}
kill(daemonPid, () => process.exit());
});
}
try {
// Throws an exception if the Theia process doesn't exist anymore.
process.kill(theiaPid, 0);
} catch {
psTree(daemonPid, function (_, children) {
for (const { PID } of children) {
kill(PID);
}
kill(daemonPid, () => process.exit());
});
}
}, 1000);

View File

@@ -8,192 +8,183 @@ 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,
LibraryLocation,
LibraryPackage,
LibraryService,
} from '../common/protocol';
import { ConfigServiceImpl } from './config-service-impl';
@injectable()
export class ExamplesServiceImpl implements ExamplesService {
@inject(SketchesServiceImpl)
protected readonly sketchesService: SketchesServiceImpl;
@inject(SketchesServiceImpl)
protected readonly sketchesService: SketchesServiceImpl;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
@inject(ConfigServiceImpl)
protected readonly configService: ConfigServiceImpl;
@inject(ConfigServiceImpl)
protected readonly configService: ConfigServiceImpl;
protected _all: SketchContainer[] | undefined;
protected _all: SketchContainer[] | undefined;
@postConstruct()
protected init(): void {
this.builtIns();
@postConstruct()
protected init(): void {
this.builtIns();
}
async builtIns(): Promise<SketchContainer[]> {
if (this._all) {
return this._all;
}
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))
);
return this._all;
}
async builtIns(): Promise<SketchContainer[]> {
if (this._all) {
return this._all;
}
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))
);
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[];
}> {
const user: SketchContainer[] = [];
const current: SketchContainer[] = [];
const any: SketchContainer[] = [];
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
) {
current.push(container);
} else {
any.push(container);
}
}
return { user, current, any };
}
// 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[];
}> {
const user: SketchContainer[] = [];
const current: SketchContainer[] = [];
const any: SketchContainer[] = [];
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
) {
current.push(container);
} else {
any.push(container);
}
}
return { user, current, any };
}
/**
* The CLI provides direct FS paths to the examples so that menus and menu groups cannot be built for the UI by traversing the
* 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));
if (installDirUri) {
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();
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();
if (subIsDir) {
const sketch = await this.tryLoadSketch(subPath);
if (!sketch) {
const container = await this.load(subPath);
if (
container.children.length ||
container.sketches.length
) {
children.push(container);
}
} else {
sketches.push(sketch);
}
}
}
return {
label,
children,
sketches,
};
/**
* The CLI provides direct FS paths to the examples so that menus and menu groups cannot be built for the UI by traversing the
* 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));
if (installDirUri) {
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();
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();
if (subIsDir) {
const sketch = await this.tryLoadSketch(subPath);
if (!sketch) {
const container = await this.load(subPath);
if (container.children.length || container.sketches.length) {
children.push(container);
}
} else {
sketches.push(sketch);
}
}
}
const sketches = await Promise.all(
paths.map((path) => this.tryLoadSketch(path))
);
return {
label,
children: [],
sketches: sketches.filter(notEmpty),
};
}
// Built-ins are included inside the IDE.
protected async load(path: string): Promise<SketchContainer> {
if (!(await promisify(fs.exists)(path))) {
throw new Error('Examples are not available');
}
const stat = await promisify(fs.stat)(path);
if (!stat.isDirectory) {
throw new Error(`${path} is not a directory.`);
}
const names = await promisify(fs.readdir)(path);
const sketches: Sketch[] = [];
const children: SketchContainer[] = [];
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);
if (sketch) {
sketches.push(sketch);
} else {
const child = await this.load(p);
children.push(child);
}
}
}
const label = basename(path);
return {
}
return {
label,
children,
sketches,
};
}
protected async group(paths: string[]): Promise<Map<string, fs.Stats>> {
const map = new Map<string, fs.Stats>();
for (const path of paths) {
const stat = await promisify(fs.stat)(path);
map.set(path, stat);
};
}
return map;
}
}
const sketches = await Promise.all(
paths.map((path) => this.tryLoadSketch(path))
);
return {
label,
children: [],
sketches: sketches.filter(notEmpty),
};
}
protected async tryLoadSketch(path: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchesService.loadSketch(
FileUri.create(path).toString()
);
return sketch;
} catch {
return undefined;
}
// Built-ins are included inside the IDE.
protected async load(path: string): Promise<SketchContainer> {
if (!(await promisify(fs.exists)(path))) {
throw new Error('Examples are not available');
}
const stat = await promisify(fs.stat)(path);
if (!stat.isDirectory) {
throw new Error(`${path} is not a directory.`);
}
const names = await promisify(fs.readdir)(path);
const sketches: Sketch[] = [];
const children: SketchContainer[] = [];
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);
if (sketch) {
sketches.push(sketch);
} else {
const child = await this.load(p);
children.push(child);
}
}
}
const label = basename(path);
return {
label,
children,
sketches,
};
}
protected async group(paths: string[]): Promise<Map<string, fs.Stats>> {
const map = new Map<string, fs.Stats>();
for (const path of paths) {
const stat = await promisify(fs.stat)(path);
map.set(path, stat);
}
return map;
}
protected async tryLoadSketch(path: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchesService.loadSketch(
FileUri.create(path).toString()
);
return sketch;
} catch {
return undefined;
}
}
}

View File

@@ -5,95 +5,87 @@ import { join } from 'path';
import { spawn } from 'child_process';
export async function getExecPath(
commandName: string,
onError: (error: Error) => void = (error) => console.log(error),
versionArg?: string | undefined,
inBinDir?: boolean
commandName: string,
onError: (error: Error) => void = (error) => console.log(error),
versionArg?: string | undefined,
inBinDir?: boolean
): Promise<string> {
const execName = `${commandName}${os.platform() === 'win32' ? '.exe' : ''}`;
const relativePath = ['..', '..', 'build'];
if (inBinDir) {
relativePath.push('bin');
}
const buildCommand = join(__dirname, ...relativePath, execName);
if (!versionArg) {
return buildCommand;
}
const versionRegexp = /\d+\.\d+\.\d+/;
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))
);
if (!pathCommand) {
return buildCommand;
}
const pathVersion = await spawnCommand(
`"${pathCommand}"`,
[versionArg],
onError
);
const pathShortVersion = (pathVersion.match(versionRegexp) || [])[0];
if (semver.gt(pathShortVersion, buildShortVersion)) {
return pathCommand;
}
const execName = `${commandName}${os.platform() === 'win32' ? '.exe' : ''}`;
const relativePath = ['..', '..', 'build'];
if (inBinDir) {
relativePath.push('bin');
}
const buildCommand = join(__dirname, ...relativePath, execName);
if (!versionArg) {
return buildCommand;
}
const versionRegexp = /\d+\.\d+\.\d+/;
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))
);
if (!pathCommand) {
return buildCommand;
}
const pathVersion = await spawnCommand(
`"${pathCommand}"`,
[versionArg],
onError
);
const pathShortVersion = (pathVersion.match(versionRegexp) || [])[0];
if (semver.gt(pathShortVersion, buildShortVersion)) {
return pathCommand;
}
return buildCommand;
}
export function spawnCommand(
command: string,
args: string[],
onError: (error: Error) => void = (error) => console.log(error)
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) => {
onError(error);
reject(error);
});
cp.on('exit', (code, signal) => {
if (code === 0) {
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);
reject(error);
return;
}
if (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}`
);
onError(error);
reject(error);
return;
}
});
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) => {
onError(error);
reject(error);
});
cp.on('exit', (code, signal) => {
if (code === 0) {
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);
reject(error);
return;
}
if (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}`);
onError(error);
reject(error);
return;
}
});
});
}

View File

@@ -6,27 +6,27 @@ import { ExecutableService } from '../common/protocol/executable-service';
@injectable()
export class ExecutableServiceImpl implements ExecutableService {
@inject(ILogger)
protected logger: ILogger;
@inject(ILogger)
protected logger: ILogger;
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)),
]);
return {
clangdUri: FileUri.create(clangd).toString(),
cliUri: FileUri.create(cli).toString(),
lsUri: FileUri.create(ls).toString(),
};
}
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)),
]);
return {
clangdUri: FileUri.create(clangd).toString(),
cliUri: FileUri.create(cli).toString(),
lsUri: FileUri.create(ls).toString(),
};
}
protected onError(error: Error): void {
this.logger.error(error);
}
protected onError(error: Error): void {
this.logger.error(error);
}
}

View File

@@ -6,74 +6,74 @@ import { ArduinoDaemonImpl } from './arduino-daemon-impl';
@injectable()
export abstract class GrpcClientProvider<C> {
@inject(ILogger)
protected readonly logger: ILogger;
@inject(ILogger)
protected readonly logger: ILogger;
@inject(ArduinoDaemonImpl)
protected readonly daemon: ArduinoDaemonImpl;
@inject(ArduinoDaemonImpl)
protected readonly daemon: ArduinoDaemonImpl;
@inject(ConfigServiceImpl)
protected readonly configService: ConfigServiceImpl;
@inject(ConfigServiceImpl)
protected readonly configService: ConfigServiceImpl;
protected _port: string | number | undefined;
protected _client: C | Error | undefined;
protected _port: string | number | undefined;
protected _client: C | Error | undefined;
@postConstruct()
protected init(): void {
const updateClient = () => {
const cliConfig = this.configService.cliConfiguration;
this.reconcileClient(cliConfig ? cliConfig.daemon.port : undefined);
};
this.configService.onConfigChange(updateClient);
this.daemon.ready.then(updateClient);
this.daemon.onDaemonStopped(() => {
if (this._client && !(this._client instanceof Error)) {
this.close(this._client);
}
this._client = undefined;
this._port = undefined;
});
@postConstruct()
protected init(): void {
const updateClient = () => {
const cliConfig = this.configService.cliConfiguration;
this.reconcileClient(cliConfig ? cliConfig.daemon.port : undefined);
};
this.configService.onConfigChange(updateClient);
this.daemon.ready.then(updateClient);
this.daemon.onDaemonStopped(() => {
if (this._client && !(this._client instanceof Error)) {
this.close(this._client);
}
this._client = undefined;
this._port = undefined;
});
}
async client(): Promise<C | Error | undefined> {
try {
await this.daemon.ready;
return this._client;
} catch (error) {
return error;
}
}
async client(): Promise<C | Error | undefined> {
try {
await this.daemon.ready;
return this._client;
} catch (error) {
return error;
}
protected async reconcileClient(
port: string | number | undefined
): Promise<void> {
if (this._port === port) {
return; // Nothing to do.
}
protected async reconcileClient(
port: string | number | undefined
): Promise<void> {
if (this._port === port) {
return; // Nothing to do.
}
this._port = port;
if (this._client && !(this._client instanceof Error)) {
this.close(this._client);
this._client = undefined;
}
if (this._port) {
try {
const client = await this.createClient(this._port);
this._client = client;
} catch (error) {
this.logger.error('Could not create client for gRPC.', error);
this._client = error;
}
}
this._port = port;
if (this._client && !(this._client instanceof Error)) {
this.close(this._client);
this._client = undefined;
}
protected abstract createClient(port: string | number): MaybePromise<C>;
protected abstract close(client: C): void;
protected get channelOptions(): Record<string, unknown> {
return {
'grpc.max_send_message_length': 512 * 1024 * 1024,
'grpc.max_receive_message_length': 512 * 1024 * 1024,
};
if (this._port) {
try {
const client = await this.createClient(this._port);
this._client = client;
} catch (error) {
this.logger.error('Could not create client for gRPC.', error);
this._client = error;
}
}
}
protected abstract createClient(port: string | number): MaybePromise<C>;
protected abstract close(client: C): void;
protected get channelOptions(): Record<string, unknown> {
return {
'grpc.max_send_message_length': 512 * 1024 * 1024,
'grpc.max_receive_message_length': 512 * 1024 * 1024,
};
}
}

View File

@@ -1,105 +1,99 @@
import {
ProgressMessage,
ResponseService,
ProgressMessage,
ResponseService,
} from '../common/protocol/response-service';
import {
DownloadProgress,
TaskProgress,
DownloadProgress,
TaskProgress,
} from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
export interface InstallResponse {
getProgress?(): DownloadProgress | undefined;
getTaskProgress(): TaskProgress | undefined;
getProgress?(): DownloadProgress | undefined;
getTaskProgress(): TaskProgress | undefined;
}
export namespace InstallWithProgress {
export interface Options {
/**
* _unknown_ progress if falsy.
*/
readonly progressId?: string;
readonly responseService: ResponseService;
}
export interface Options {
/**
* _unknown_ progress if falsy.
*/
readonly progressId?: string;
readonly responseService: ResponseService;
}
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 task = response.getTaskProgress();
if (!download && !task) {
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."
);
}
if (task) {
const message = task.getName() || task.getMessage();
if (message) {
if (progressId) {
responseService.reportProgress({
progressId,
message,
work: { done: Number.NaN, total: Number.NaN },
});
}
responseService.appendToOutput({ chunk: `${message}\n` });
}
} else if (download) {
if (download.getFile() && !localFile) {
localFile = download.getFile();
}
if (
download.getTotalSize() > 0 &&
Number.isNaN(localTotalSize)
) {
localTotalSize = download.getTotalSize();
}
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 task = response.getTaskProgress();
if (!download && !task) {
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."
);
}
if (task) {
const message = task.getName() || task.getMessage();
if (message) {
if (progressId) {
responseService.reportProgress({
progressId,
message,
work: { done: Number.NaN, total: Number.NaN },
});
}
responseService.appendToOutput({ chunk: `${message}\n` });
}
} else if (download) {
if (download.getFile() && !localFile) {
localFile = download.getFile();
}
if (download.getTotalSize() > 0 && Number.isNaN(localTotalSize)) {
localTotalSize = download.getTotalSize();
}
// This happens only once per file download.
if (download.getTotalSize() && localFile) {
responseService.appendToOutput({ chunk: `${localFile}\n` });
}
// This happens only once per file download.
if (download.getTotalSize() && localFile) {
responseService.appendToOutput({ chunk: `${localFile}\n` });
}
if (progressId && localFile) {
let work: ProgressMessage.Work | undefined = undefined;
if (
download.getDownloaded() > 0 &&
!Number.isNaN(localTotalSize)
) {
work = {
total: localTotalSize,
done: download.getDownloaded(),
};
}
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 },
});
}
localFile = '';
localTotalSize = Number.NaN;
}
}
};
}
if (progressId && localFile) {
let work: ProgressMessage.Work | undefined = undefined;
if (download.getDownloaded() > 0 && !Number.isNaN(localTotalSize)) {
work = {
total: localTotalSize,
done: download.getDownloaded(),
};
}
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 },
});
}
localFile = '';
localTotalSize = Number.NaN;
}
}
};
}
}

View File

@@ -1,24 +1,24 @@
import { injectable, inject } from 'inversify';
import {
LibraryDependency,
LibraryLocation,
LibraryPackage,
LibraryService,
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';
@@ -28,364 +28,360 @@ import { InstallWithProgress } from './grpc-installable';
@injectable()
export class LibraryServiceImpl
extends CoreClientAware
implements LibraryService
extends CoreClientAware
implements LibraryService
{
@inject(ILogger)
protected logger: ILogger;
@inject(ILogger)
protected logger: ILogger;
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(NotificationServiceServer)
protected readonly notificationServer: NotificationServiceServer;
@inject(NotificationServiceServer)
protected readonly notificationServer: NotificationServiceServer;
async search(options: { query?: string }): Promise<LibraryPackage[]> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
async search(options: { query?: string }): Promise<LibraryPackage[]> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
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 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 installedLibs = installedLibsResp.getInstalledLibrariesList();
const installedLibsIdx = new Map<string, InstalledLibrary>();
for (const installedLib of installedLibs) {
if (installedLib.hasLibrary()) {
const lib = installedLib.getLibrary();
if (lib) {
installedLibsIdx.set(lib.getRealName(), installedLib);
}
}
}
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())
.slice(0, 50)
.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();
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
);
const installedLibs = installedLibsResp.getInstalledLibrariesList();
const installedLibsIdx = new Map<string, InstalledLibrary>();
for (const installedLib of installedLibs) {
if (installedLib.hasLibrary()) {
const lib = installedLib.getLibrary();
if (lib) {
installedLibsIdx.set(lib.getRealName(), installedLib);
}
});
return items;
}
async list({
fqbn,
}: {
fqbn?: string | undefined;
}): Promise<LibraryPackage[]> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new LibraryListRequest();
req.setInstance(instance);
if (fqbn) {
// Only get libraries from the cores when the FQBN is defined. Otherwise, we retrieve user installed libraries only.
req.setAll(true); // https://github.com/arduino/arduino-ide/pull/303#issuecomment-815556447
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;
}
}
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())
.slice(0, 50)
.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();
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 items;
}
async list({
fqbn,
}: {
fqbn?: string | undefined;
}): Promise<LibraryPackage[]> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new LibraryListRequest();
req.setInstance(instance);
if (fqbn) {
// Only get libraries from the cores when the FQBN is defined. Otherwise, we retrieve user installed libraries only.
req.setAll(true); // https://github.com/arduino/arduino-ide/pull/303#issuecomment-815556447
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;
}
// 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;
}
resolve(r);
});
// 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;
}
);
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);
}
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}.`);
}
}
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(),
}
)
);
});
// It's a hack to handle https://github.com/arduino/arduino-cli/issues/1262 gracefully.
if (message.indexOf('unknown package') !== -1) {
resolve(undefined);
return;
}
);
return filterSelf
? dependencies.filter(({ name }) => name !== item.name)
: dependencies;
}
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 coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new LibraryInstallRequest();
req.setInstance(instance);
req.setName(item.name);
req.setVersion(version);
if (options.installDependencies === false) {
req.setNoDeps(true);
}
console.info('>>> Starting library package installation...', item);
const resp = client.libraryInstall(req);
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(),
});
reject(error);
});
reject(error);
return;
}
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);
}
const items = await this.search({});
const updated =
items.find((other) => LibraryPackage.equals(other, item)) || item;
this.notificationServer.notifyLibraryInstalled({ item: updated });
console.info('<<< Library package installation done.', item);
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}.`);
}
}
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;
}
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 coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new LibraryInstallRequest();
req.setInstance(instance);
req.setName(item.name);
req.setVersion(version);
if (options.installDependencies === false) {
req.setNoDeps(true);
}
async installZip({
zipUri,
console.info('>>> Starting library package installation...', item);
const resp = client.libraryInstall(req);
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(),
});
reject(error);
});
});
const items = await this.search({});
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> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new ZipLibraryInstallRequest();
req.setPath(FileUri.fsPath(zipUri));
req.setInstance(instance);
if (typeof overwrite === 'boolean') {
req.setOverwrite(overwrite);
}
const resp = client.zipLibraryInstall(req);
resp.on(
'data',
InstallWithProgress.createDataCallback({
progressId,
overwrite,
}: {
zipUri: string;
progressId?: string;
overwrite?: boolean;
}): Promise<void> {
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new ZipLibraryInstallRequest();
req.setPath(FileUri.fsPath(zipUri));
req.setInstance(instance);
if (typeof overwrite === 'boolean') {
req.setOverwrite(overwrite);
}
const resp = client.zipLibraryInstall(req);
resp.on(
'data',
InstallWithProgress.createDataCallback({
progressId,
responseService: this.responseService,
})
);
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);
});
}
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> {
const { item, progressId } = options;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
async uninstall(options: {
item: LibraryPackage;
progressId?: string;
}): Promise<void> {
const { item, progressId } = options;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new LibraryUninstallRequest();
req.setInstance(instance);
req.setName(item.name);
req.setVersion(item.installedVersion!);
const req = new LibraryUninstallRequest();
req.setInstance(instance);
req.setName(item.name);
req.setVersion(item.installedVersion!);
console.info('>>> Starting library package uninstallation...', item);
const resp = client.libraryUninstall(req);
resp.on(
'data',
InstallWithProgress.createDataCallback({
progressId,
responseService: this.responseService,
})
);
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);
});
console.info('>>> Starting library package uninstallation...', item);
const resp = client.libraryUninstall(req);
resp.on(
'data',
InstallWithProgress.createDataCallback({
progressId,
responseService: this.responseService,
})
);
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);
});
this.notificationServer.notifyLibraryUninstalled({ item });
console.info('<<< Library package uninstallation done.', item);
}
this.notificationServer.notifyLibraryUninstalled({ item });
console.info('<<< Library package uninstallation done.', item);
}
dispose(): void {
this.logger.info('>>> Disposing library service...');
this.logger.info('<<< Disposed library service.');
}
dispose(): void {
this.logger.info('>>> Disposing library service...');
this.logger.info('<<< Disposed library service.');
}
}
function toLibrary(
pkg: Partial<LibraryPackage>,
lib: LibraryRelease | Library,
availableVersions: string[]
pkg: Partial<LibraryPackage>,
lib: LibraryRelease | Library,
availableVersions: string[]
): LibraryPackage {
return {
name: '',
label: '',
exampleUris: [],
installable: false,
deprecated: false,
location: 0,
...pkg,
return {
name: '',
label: '',
exampleUris: [],
installable: false,
deprecated: false,
location: 0,
...pkg,
author: lib.getAuthor(),
availableVersions,
includes: lib.getProvidesIncludesList(),
description: lib.getSentence(),
moreInfoLink: lib.getWebsite(),
summary: lib.getParagraph(),
};
author: lib.getAuthor(),
availableVersions,
includes: lib.getProvidesIncludesList(),
description: lib.getSentence(),
moreInfoLink: lib.getWebsite(),
summary: lib.getParagraph(),
};
}

View File

@@ -6,21 +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
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
);
}
createClient(port: string | number): MonitorServiceClient {
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
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();
}
close(client: MonitorServiceClient): void {
client.close();
}
}

View File

@@ -5,235 +5,229 @@ 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,
MonitorService,
MonitorServiceClient,
MonitorConfig,
MonitorError,
Status,
} from '../../common/protocol/monitor-service';
import {
StreamingOpenRequest,
StreamingOpenResponse,
MonitorConfig as GrpcMonitorConfig,
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';
interface ErrorWithCode extends Error {
readonly code: number;
readonly code: number;
}
namespace ErrorWithCode {
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
);
code = mapping.get(message);
}
return {
message,
code,
config,
};
}
function is(error: Error & { code?: number }): error is ErrorWithCode {
return typeof error.code === 'number';
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
);
code = mapping.get(message);
}
return {
message,
code,
config,
};
}
function is(error: Error & { code?: number }): error is ErrorWithCode {
return typeof error.code === 'number';
}
}
@injectable()
export class MonitorServiceImpl implements MonitorService {
@inject(ILogger)
@named('monitor-service')
protected readonly logger: ILogger;
@inject(ILogger)
@named('monitor-service')
protected readonly logger: ILogger;
@inject(MonitorClientProvider)
protected readonly monitorClientProvider: MonitorClientProvider;
@inject(MonitorClientProvider)
protected readonly monitorClientProvider: MonitorClientProvider;
protected client?: MonitorServiceClient;
protected connection?: {
duplex: ClientDuplexStream<StreamingOpenRequest, StreamingOpenResponse>;
config: MonitorConfig;
};
protected messages: string[] = [];
protected onMessageDidReadEmitter = new Emitter<void>();
protected client?: MonitorServiceClient;
protected connection?: {
duplex: ClientDuplexStream<StreamingOpenRequest, StreamingOpenResponse>;
config: MonitorConfig;
};
protected messages: string[] = [];
protected onMessageDidReadEmitter = new Emitter<void>();
setClient(client: MonitorServiceClient | undefined): void {
this.client = client;
setClient(client: MonitorServiceClient | undefined): void {
this.client = client;
}
dispose(): void {
this.logger.info('>>> Disposing monitor service...');
if (this.connection) {
this.disconnect();
}
this.logger.info('<<< Disposed monitor service.');
this.client = undefined;
}
dispose(): void {
this.logger.info('>>> Disposing monitor service...');
if (this.connection) {
this.disconnect();
}
this.logger.info('<<< Disposed monitor service.');
this.client = undefined;
async connect(config: MonitorConfig): Promise<Status> {
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;
}
const client = await this.monitorClientProvider.client();
if (!client) {
return Status.NOT_CONNECTED;
}
if (client instanceof Error) {
return { message: client.message };
}
const duplex = client.streamingOpen();
this.connection = { duplex, config };
async connect(config: MonitorConfig): Promise<Status> {
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;
}
const client = await this.monitorClientProvider.client();
if (!client) {
return Status.NOT_CONNECTED;
}
if (client instanceof Error) {
return { message: client.message };
}
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(
'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();
const monitorConfig = new GrpcMonitorConfig();
monitorConfig.setType(this.mapType(type));
monitorConfig.setTarget(port.address);
if (config.baudRate !== undefined) {
monitorConfig.setAdditionalConfig(
Struct.fromJavaScript({ BaudRate: config.baudRate })
);
}
req.setConfig(monitorConfig);
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)}.`
);
resolve(Status.OK);
});
return;
}
this.disconnect().then(() => resolve(Status.NOT_CONNECTED));
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)
);
async disconnect(reason?: MonitorError): Promise<Status> {
try {
if (
!this.connection &&
reason &&
reason.code === MonitorError.ErrorCodes.CLIENT_CANCEL
) {
return Status.OK;
}
this.logger.info('>>> Disposing monitor connection...');
if (!this.connection) {
this.logger.warn('<<< Not connected. Nothing to dispose.');
return Status.NOT_CONNECTED;
}
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.connection = undefined;
return Status.OK;
} finally {
this.messages.length = 0;
}
}
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)
);
async send(message: string): Promise<Status> {
if (!this.connection) {
return Status.NOT_CONNECTED;
}
const req = new StreamingOpenRequest();
req.setData(new TextEncoder().encode(message));
return new Promise<Status>((resolve) => {
if (this.connection) {
this.connection.duplex.write(req, () => {
resolve(Status.OK);
});
return;
}
this.disconnect().then(() => resolve(Status.NOT_CONNECTED));
const { type, port } = config;
const req = new StreamingOpenRequest();
const monitorConfig = new GrpcMonitorConfig();
monitorConfig.setType(this.mapType(type));
monitorConfig.setTarget(port.address);
if (config.baudRate !== undefined) {
monitorConfig.setAdditionalConfig(
Struct.fromJavaScript({ BaudRate: config.baudRate })
);
}
req.setConfig(monitorConfig);
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)}.`
);
resolve(Status.OK);
});
}
return;
}
this.disconnect().then(() => resolve(Status.NOT_CONNECTED));
});
}
async request(): Promise<{ message: string }> {
const message = this.messages.shift();
if (message) {
return { message };
}
return new Promise<{ message: string }>((resolve) => {
const toDispose = this.onMessageDidReadEmitter.event(() => {
toDispose.dispose();
resolve(this.request());
});
async disconnect(reason?: MonitorError): Promise<Status> {
try {
if (
!this.connection &&
reason &&
reason.code === MonitorError.ErrorCodes.CLIENT_CANCEL
) {
return Status.OK;
}
this.logger.info('>>> Disposing monitor connection...');
if (!this.connection) {
this.logger.warn('<<< Not connected. Nothing to dispose.');
return Status.NOT_CONNECTED;
}
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.connection = undefined;
return Status.OK;
} finally {
this.messages.length = 0;
}
}
async send(message: string): Promise<Status> {
if (!this.connection) {
return Status.NOT_CONNECTED;
}
const req = new StreamingOpenRequest();
req.setData(new TextEncoder().encode(message));
return new Promise<Status>((resolve) => {
if (this.connection) {
this.connection.duplex.write(req, () => {
resolve(Status.OK);
});
}
return;
}
this.disconnect().then(() => resolve(Status.NOT_CONNECTED));
});
}
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;
}
async request(): Promise<{ message: string }> {
const message = this.messages.shift();
if (message) {
return { message };
}
return new Promise<{ message: string }>((resolve) => {
const toDispose = this.onMessageDidReadEmitter.event(() => {
toDispose.dispose();
resolve(this.request());
});
});
}
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;
}
}
}

View File

@@ -4,7 +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();
}
async getUri(fsPath: string): Promise<string> {
return FileUri.create(fsPath).toString();
}
}

View File

@@ -1,87 +1,79 @@
import { injectable } from 'inversify';
import {
NotificationServiceServer,
NotificationServiceClient,
AttachedBoardsChangeEvent,
BoardsPackage,
LibraryPackage,
Config,
Sketch,
NotificationServiceServer,
NotificationServiceClient,
AttachedBoardsChangeEvent,
BoardsPackage,
LibraryPackage,
Config,
Sketch,
} from '../common/protocol';
@injectable()
export class NotificationServiceServerImpl
implements NotificationServiceServer
implements NotificationServiceServer
{
protected readonly clients: NotificationServiceClient[] = [];
protected readonly clients: NotificationServiceClient[] = [];
notifyIndexUpdated(): void {
this.clients.forEach((client) => client.notifyIndexUpdated());
}
notifyIndexUpdated(): void {
this.clients.forEach((client) => client.notifyIndexUpdated());
}
notifyDaemonStarted(): void {
this.clients.forEach((client) => client.notifyDaemonStarted());
}
notifyDaemonStarted(): void {
this.clients.forEach((client) => client.notifyDaemonStarted());
}
notifyDaemonStopped(): void {
this.clients.forEach((client) => client.notifyDaemonStopped());
}
notifyDaemonStopped(): void {
this.clients.forEach((client) => client.notifyDaemonStopped());
}
notifyPlatformInstalled(event: { item: BoardsPackage }): void {
this.clients.forEach((client) => client.notifyPlatformInstalled(event));
}
notifyPlatformInstalled(event: { item: BoardsPackage }): void {
this.clients.forEach((client) => client.notifyPlatformInstalled(event));
}
notifyPlatformUninstalled(event: { item: BoardsPackage }): void {
this.clients.forEach((client) =>
client.notifyPlatformUninstalled(event)
);
}
notifyPlatformUninstalled(event: { item: BoardsPackage }): void {
this.clients.forEach((client) => client.notifyPlatformUninstalled(event));
}
notifyLibraryInstalled(event: { item: LibraryPackage }): void {
this.clients.forEach((client) => client.notifyLibraryInstalled(event));
}
notifyLibraryInstalled(event: { item: LibraryPackage }): void {
this.clients.forEach((client) => client.notifyLibraryInstalled(event));
}
notifyLibraryUninstalled(event: { item: LibraryPackage }): void {
this.clients.forEach((client) =>
client.notifyLibraryUninstalled(event)
);
}
notifyLibraryUninstalled(event: { item: LibraryPackage }): void {
this.clients.forEach((client) => client.notifyLibraryUninstalled(event));
}
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.clients.forEach((client) =>
client.notifyAttachedBoardsChanged(event)
);
}
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.clients.forEach((client) => client.notifyAttachedBoardsChanged(event));
}
notifyConfigChanged(event: { config: Config | undefined }): void {
this.clients.forEach((client) => client.notifyConfigChanged(event));
}
notifyConfigChanged(event: { config: Config | undefined }): void {
this.clients.forEach((client) => client.notifyConfigChanged(event));
}
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
this.clients.forEach((client) =>
client.notifyRecentSketchesChanged(event)
);
}
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
this.clients.forEach((client) => client.notifyRecentSketchesChanged(event));
}
setClient(client: NotificationServiceClient): void {
this.clients.push(client);
}
setClient(client: NotificationServiceClient): void {
this.clients.push(client);
}
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.'
);
return;
}
this.clients.splice(index, 1);
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.'
);
return;
}
this.clients.splice(index, 1);
}
dispose(): void {
for (const client of this.clients) {
this.disposeClient(client);
}
this.clients.length = 0;
dispose(): void {
for (const client of this.clients) {
this.disposeClient(client);
}
this.clients.length = 0;
}
}

View File

@@ -12,17 +12,17 @@ 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,
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,
ArchiveSketchRequest,
LoadSketchRequest,
} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/;
@@ -31,313 +31,300 @@ const prefix = '.arduinoIDE-unsaved';
@injectable()
export class SketchesServiceImpl
extends CoreClientAware
implements SketchesService
extends CoreClientAware
implements SketchesService
{
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(NotificationServiceServerImpl)
protected readonly notificationService: NotificationServiceServerImpl;
@inject(NotificationServiceServerImpl)
protected readonly notificationService: NotificationServiceServerImpl;
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
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();
sketchbookPath = FileUri.fsPath(sketchDirUri);
if (!(await promisify(fs.exists)(sketchbookPath))) {
await promisify(fs.mkdir)(sketchbookPath, { recursive: true });
}
} else {
sketchbookPath = FileUri.fsPath(uri);
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
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();
sketchbookPath = FileUri.fsPath(sketchDirUri);
if (!(await promisify(fs.exists)(sketchbookPath))) {
await promisify(fs.mkdir)(sketchbookPath, { recursive: true });
}
} else {
sketchbookPath = FileUri.fsPath(uri);
}
const container: SketchContainerWithDetails = {
label: uri ? path.basename(sketchbookPath) : 'Sketchbook',
sketches: [],
children: [],
};
if (!(await promisify(fs.exists)(sketchbookPath))) {
return container;
}
const stat = await promisify(fs.stat)(sketchbookPath);
if (!stat.isDirectory()) {
return container;
}
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/**',
]) {
if (!skip && minimatch(childFsPath, pattern)) {
skip = true;
}
}
const container: SketchContainerWithDetails = {
label: uri ? path.basename(sketchbookPath) : 'Sketchbook',
sketches: [],
children: [],
};
if (!(await promisify(fs.exists)(sketchbookPath))) {
return container;
if (skip) {
continue;
}
const stat = await promisify(fs.stat)(sketchbookPath);
if (!stat.isDirectory()) {
return container;
}
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/**',
]) {
if (!skip && minimatch(childFsPath, pattern)) {
skip = true;
}
}
if (skip) {
continue;
}
try {
const stat = await promisify(fs.stat)(childFsPath);
if (stat.isDirectory()) {
const sketch = await this._isSketchFolder(
FileUri.create(childFsPath).toString()
);
if (sketch) {
containerToLoad.sketches.push({
...sketch,
mtimeMs: stat.mtimeMs,
});
} else {
const childContainer: SketchContainerWithDetails = {
label: name,
children: [],
sketches: [],
};
await recursivelyLoad(childFsPath, childContainer);
if (!SketchContainer.isEmpty(childContainer)) {
containerToLoad.children.push(childContainer);
}
}
}
} catch {
console.warn(`Could not load sketch from ${childFsPath}.`);
}
}
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.`
);
return container;
}
async loadSketch(uri: string): Promise<SketchWithDetails> {
const { client, instance } = await this.coreClient();
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,
});
});
}
);
return sketch;
}
async maybeLoadSketch(uri: string): Promise<Sketch | undefined> {
return this._isSketchFolder(uri);
}
private get recentSketchesFsPath(): Promise<string> {
return this.envVariableServer
.getConfigDirUri()
.then((uri) =>
path.join(FileUri.fsPath(uri), 'recent-sketches.json')
);
}
private async loadRecentSketches(
fsPath: string
): Promise<Record<string, number>> {
let data: Record<string, number> = {};
try {
const raw = await promisify(fs.readFile)(fsPath, {
encoding: 'utf8',
});
data = JSON.parse(raw);
} catch {}
return data;
}
async markAsRecentlyOpened(uri: string): Promise<void> {
let sketch: Sketch | undefined = undefined;
try {
sketch = await this.loadSketch(uri);
const stat = await promisify(fs.stat)(childFsPath);
if (stat.isDirectory()) {
const sketch = await this._isSketchFolder(
FileUri.create(childFsPath).toString()
);
if (sketch) {
containerToLoad.sketches.push({
...sketch,
mtimeMs: stat.mtimeMs,
});
} else {
const childContainer: SketchContainerWithDetails = {
label: name,
children: [],
sketches: [],
};
await recursivelyLoad(childFsPath, childContainer);
if (!SketchContainer.isEmpty(childContainer)) {
containerToLoad.children.push(childContainer);
}
}
}
} catch {
return;
console.warn(`Could not load sketch from ${childFsPath}.`);
}
if (await this.isTemp(sketch)) {
return;
}
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.`
);
return container;
}
async loadSketch(uri: string): Promise<SketchWithDetails> {
const { client, instance } = await this.coreClient();
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,
});
});
});
return sketch;
}
const fsPath = await this.recentSketchesFsPath;
const data = await this.loadRecentSketches(fsPath);
const now = Date.now();
data[sketch.uri] = now;
async maybeLoadSketch(uri: string): Promise<Sketch | undefined> {
return this._isSketchFolder(uri);
}
let toDeleteUri: string | undefined = undefined;
if (Object.keys(data).length > 10) {
let min = Number.MAX_SAFE_INTEGER;
for (const uri of Object.keys(data)) {
if (min > data[uri]) {
min = data[uri];
toDeleteUri = uri;
}
}
}
private get recentSketchesFsPath(): Promise<string> {
return this.envVariableServer
.getConfigDirUri()
.then((uri) => path.join(FileUri.fsPath(uri), 'recent-sketches.json'));
}
if (toDeleteUri) {
delete data[toDeleteUri];
}
private async loadRecentSketches(
fsPath: string
): Promise<Record<string, number>> {
let data: Record<string, number> = {};
try {
const raw = await promisify(fs.readFile)(fsPath, {
encoding: 'utf8',
});
data = JSON.parse(raw);
} catch {}
return data;
}
await promisify(fs.writeFile)(fsPath, JSON.stringify(data, null, 2));
this.recentlyOpenedSketches().then((sketches) =>
this.notificationService.notifyRecentSketchesChanged({ sketches })
);
async markAsRecentlyOpened(uri: string): Promise<void> {
let sketch: Sketch | undefined = undefined;
try {
sketch = await this.loadSketch(uri);
} catch {
return;
}
if (await this.isTemp(sketch)) {
return;
}
async recentlyOpenedSketches(): Promise<Sketch[]> {
const configDirUri = await this.envVariableServer.getConfigDirUri();
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',
});
data = JSON.parse(raw);
} catch {}
const fsPath = await this.recentSketchesFsPath;
const data = await this.loadRecentSketches(fsPath);
const now = Date.now();
data[sketch.uri] = now;
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 {}
let toDeleteUri: string | undefined = undefined;
if (Object.keys(data).length > 10) {
let min = Number.MAX_SAFE_INTEGER;
for (const uri of Object.keys(data)) {
if (min > data[uri]) {
min = data[uri];
toDeleteUri = uri;
}
return sketches;
}
}
async cloneExample(uri: string): Promise<Sketch> {
if (toDeleteUri) {
delete data[toDeleteUri];
}
await promisify(fs.writeFile)(fsPath, JSON.stringify(data, null, 2));
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'
);
let data: Record<string, number> = {};
try {
const raw = await promisify(fs.readFile)(fsPath, {
encoding: 'utf8',
});
data = JSON.parse(raw);
} catch {}
const sketches: SketchWithDetails[] = [];
for (const uri of Object.keys(data).sort(
(left, right) => data[right] - data[left]
)) {
try {
const sketch = await this.loadSketch(uri);
const parentPath = await new Promise<string>((resolve, reject) => {
temp.mkdir({ prefix }, (err, dirPath) => {
if (err) {
reject(err);
return;
}
resolve(dirPath);
});
});
const destinationUri = FileUri.create(
path.join(parentPath, sketch.name)
).toString();
const copiedSketchUri = await this.copy(sketch, { destinationUri });
return this.loadSketch(copiedSketchUri);
sketches.push(sketch);
} catch {}
}
async createNewSketch(): Promise<Sketch> {
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) => {
if (err) {
reject(err);
return;
}
resolve(dirPath);
});
});
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++) {
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))
) {
continue;
}
return sketches;
}
sketchName = sketchNameCandidate;
break;
async cloneExample(uri: string): Promise<Sketch> {
const sketch = await this.loadSketch(uri);
const parentPath = await new Promise<string>((resolve, reject) => {
temp.mkdir({ prefix }, (err, dirPath) => {
if (err) {
reject(err);
return;
}
resolve(dirPath);
});
});
const destinationUri = FileUri.create(
path.join(parentPath, sketch.name)
).toString();
const copiedSketchUri = await this.copy(sketch, { destinationUri });
return this.loadSketch(copiedSketchUri);
}
if (!sketchName) {
throw new Error('Cannot create a unique sketch name');
async createNewSketch(): Promise<Sketch> {
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) => {
if (err) {
reject(err);
return;
}
resolve(dirPath);
});
});
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++) {
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))) {
continue;
}
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() {
sketchName = sketchNameCandidate;
break;
}
if (!sketchName) {
throw new Error('Cannot create a unique sketch name');
}
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() {
// put your setup code here, to run once:
}
@@ -347,185 +334,174 @@ void loop() {
}
`,
{ encoding: 'utf8' }
);
return this.loadSketch(FileUri.create(sketchDir).toString());
{ encoding: 'utf8' }
);
return this.loadSketch(FileUri.create(sketchDir).toString());
}
async getSketchFolder(uri: string): Promise<Sketch | undefined> {
if (!uri) {
return undefined;
}
let currentUri = new URI(uri);
while (currentUri && !currentUri.path.isRoot) {
const sketch = await this._isSketchFolder(currentUri.toString());
if (sketch) {
return sketch;
}
currentUri = currentUri.parent;
}
return undefined;
}
async isSketchFolder(uri: string): Promise<boolean> {
const sketch = await this._isSketchFolder(uri);
return !!sketch;
}
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 {}
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') {
try {
const sketch = await this.loadSketch(
FileUri.create(fsPath).toString()
);
return sketch;
} catch {}
}
}
}
return undefined;
}
async isTemp(sketch: Sketch): Promise<boolean> {
let sketchPath = FileUri.fsPath(sketch.uri);
let temp = os.tmpdir();
// Note: VS Code URI normalizes the drive letter. `C:` will be converted into `c:`.
// https://github.com/Microsoft/vscode/issues/68325#issuecomment-462239992
if (isWindows) {
if (WIN32_DRIVE_REGEXP.exec(sketchPath)) {
sketchPath = firstToLowerCase(sketchPath);
}
if (WIN32_DRIVE_REGEXP.exec(temp)) {
temp = firstToLowerCase(temp);
}
}
return sketchPath.indexOf(prefix) !== -1 && sketchPath.startsWith(temp);
}
async copy(
sketch: Sketch,
{ destinationUri }: { destinationUri: string }
): Promise<string> {
const source = FileUri.fsPath(sketch.uri);
const exists = await promisify(fs.exists)(source);
if (!exists) {
throw new Error(`Sketch does not exist: ${sketch}`);
}
// Nothing to do when source and destination are the same.
if (sketch.uri === destinationUri) {
await this.loadSketch(sketch.uri); // Sanity check.
return sketch.uri;
}
async getSketchFolder(uri: string): Promise<Sketch | undefined> {
if (!uri) {
return undefined;
}
let currentUri = new URI(uri);
while (currentUri && !currentUri.path.isRoot) {
const sketch = await this._isSketchFolder(currentUri.toString());
if (sketch) {
return sketch;
const copy = async (sourcePath: string, destinationPath: string) => {
return new Promise<void>((resolve, reject) => {
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`);
if (oldPath !== newPath) {
await promisify(fs.rename)(oldPath, newPath);
}
currentUri = currentUri.parent;
}
return undefined;
}
async isSketchFolder(uri: string): Promise<boolean> {
const sketch = await this._isSketchFolder(uri);
return !!sketch;
}
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 {}
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'
) {
try {
const sketch = await this.loadSketch(
FileUri.create(fsPath).toString()
);
return sketch;
} catch {}
}
}
}
return undefined;
}
async isTemp(sketch: Sketch): Promise<boolean> {
let sketchPath = FileUri.fsPath(sketch.uri);
let temp = os.tmpdir();
// Note: VS Code URI normalizes the drive letter. `C:` will be converted into `c:`.
// https://github.com/Microsoft/vscode/issues/68325#issuecomment-462239992
if (isWindows) {
if (WIN32_DRIVE_REGEXP.exec(sketchPath)) {
sketchPath = firstToLowerCase(sketchPath);
}
if (WIN32_DRIVE_REGEXP.exec(temp)) {
temp = firstToLowerCase(temp);
}
}
return sketchPath.indexOf(prefix) !== -1 && sketchPath.startsWith(temp);
}
async copy(
sketch: Sketch,
{ destinationUri }: { destinationUri: string }
): Promise<string> {
const source = FileUri.fsPath(sketch.uri);
const exists = await promisify(fs.exists)(source);
if (!exists) {
throw new Error(`Sketch does not exist: ${sketch}`);
}
// Nothing to do when source and destination are the same.
if (sketch.uri === destinationUri) {
await this.loadSketch(sketch.uri); // Sanity check.
return sketch.uri;
}
const copy = async (sourcePath: string, destinationPath: string) => {
return new Promise<void>((resolve, reject) => {
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`
);
if (oldPath !== newPath) {
await promisify(fs.rename)(oldPath, newPath);
}
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,
// then move it to the desired destination.
const destination = FileUri.fsPath(destinationUri);
let tempDestination = await new Promise<string>((resolve, reject) => {
temp.track().mkdir({ prefix }, async (err, dirPath) => {
if (err) {
reject(err);
return;
}
resolve(dirPath);
});
await this.loadSketch(FileUri.create(destinationPath).toString()); // Sanity check.
resolve();
} catch (e) {
reject(e);
}
});
tempDestination = path.join(tempDestination, sketch.name);
await fs.promises.mkdir(tempDestination, { recursive: true });
await copy(source, tempDestination);
await copy(tempDestination, destination);
return FileUri.create(destination).toString();
}
async archive(sketch: Sketch, destinationUri: string): Promise<string> {
await this.loadSketch(sketch.uri); // sanity check
const { client } = await this.coreClient();
const archivePath = FileUri.fsPath(destinationUri);
// The CLI cannot override existing archives, so we have to wipe it manually: https://github.com/arduino/arduino-cli/issues/1160
if (await promisify(fs.exists)(archivePath)) {
await promisify(fs.unlink)(archivePath);
});
};
// 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,
// then move it to the desired destination.
const destination = FileUri.fsPath(destinationUri);
let tempDestination = await new Promise<string>((resolve, reject) => {
temp.track().mkdir({ prefix }, async (err, dirPath) => {
if (err) {
reject(err);
return;
}
const req = new ArchiveSketchRequest();
req.setSketchPath(FileUri.fsPath(sketch.uri));
req.setArchivePath(archivePath);
await new Promise<string>((resolve, reject) => {
client.archiveSketch(req, (err) => {
if (err) {
reject(err);
return;
}
resolve(destinationUri);
});
});
return destinationUri;
}
resolve(dirPath);
});
});
tempDestination = path.join(tempDestination, sketch.name);
await fs.promises.mkdir(tempDestination, { recursive: true });
await copy(source, tempDestination);
await copy(tempDestination, destination);
return FileUri.create(destination).toString();
}
async getIdeTempFolderUri(sketch: Sketch): Promise<string> {
const genBuildPath = await this.getIdeTempFolderPath(sketch);
return FileUri.create(genBuildPath).toString();
async archive(sketch: Sketch, destinationUri: string): Promise<string> {
await this.loadSketch(sketch.uri); // sanity check
const { client } = await this.coreClient();
const archivePath = FileUri.fsPath(destinationUri);
// The CLI cannot override existing archives, so we have to wipe it manually: https://github.com/arduino/arduino-cli/issues/1160
if (await promisify(fs.exists)(archivePath)) {
await promisify(fs.unlink)(archivePath);
}
const req = new ArchiveSketchRequest();
req.setSketchPath(FileUri.fsPath(sketch.uri));
req.setArchivePath(archivePath);
await new Promise<string>((resolve, reject) => {
client.archiveSketch(req, (err) => {
if (err) {
reject(err);
return;
}
resolve(destinationUri);
});
});
return destinationUri;
}
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');
return path.join(os.tmpdir(), `arduino-ide2-${suffix}`);
}
async getIdeTempFolderUri(sketch: Sketch): Promise<string> {
const genBuildPath = await this.getIdeTempFolderPath(sketch);
return FileUri.create(genBuildPath).toString();
}
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');
return path.join(os.tmpdir(), `arduino-ide2-${suffix}`);
}
}
interface SketchWithDetails extends Sketch {
readonly mtimeMs: number;
readonly mtimeMs: number;
}
interface SketchContainerWithDetails extends SketchContainer {
readonly label: string;
readonly children: SketchContainerWithDetails[];
readonly sketches: SketchWithDetails[];
readonly label: string;
readonly children: SketchContainerWithDetails[];
readonly sketches: SketchWithDetails[];
}

View File

@@ -1,26 +1,26 @@
import { inject, injectable, named } from 'inversify';
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
import {
BackendApplication as TheiaBackendApplication,
BackendApplicationContribution,
BackendApplicationCliContribution,
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
) {
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.'))
);
}
constructor(
@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.'))
);
}
}

View File

@@ -7,12 +7,9 @@ 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,52 +7,50 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
@injectable()
export class DefaultGitInit implements GitInit {
protected readonly toDispose = new DisposableCollection();
protected readonly toDispose = new DisposableCollection();
async init(): Promise<void> {
const { env } = process;
try {
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),
]);
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}`
);
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}`
);
return;
}
process.env.LOCAL_GIT_DIRECTORY = dir;
process.env.GIT_EXEC_PATH = execPath;
console.info(
`Using Git [${version}] from the PATH. (${path})`
);
return;
}
}
} catch (err) {
console.error(err);
async init(): Promise<void> {
const { env } = process;
try {
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),
]);
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}`
);
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}`
);
return;
}
process.env.LOCAL_GIT_DIRECTORY = dir;
process.env.GIT_EXEC_PATH = execPath;
console.info(`Using Git [${version}] from the PATH. (${path})`);
return;
}
}
} catch (err) {
console.error(err);
}
}
dispose(): void {
this.toDispose.dispose();
}
dispose(): void {
this.toDispose.dispose();
}
}

View File

@@ -5,21 +5,19 @@ import { ConfigService } from '../../../common/protocol/config-service';
@injectable()
export class DefaultWorkspaceServer extends TheiaDefaultWorkspaceServer {
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(ILogger)
protected readonly logger: ILogger;
@inject(ILogger)
protected readonly logger: ILogger;
protected async getWorkspaceURIFromCli(): Promise<string | undefined> {
try {
const config = await this.configService.getConfiguration();
return config.sketchDirUri;
} catch (err) {
this.logger.error(
`Failed to determine the sketch directory: ${err}`
);
return super.getWorkspaceURIFromCli();
}
protected async getWorkspaceURIFromCli(): Promise<string | undefined> {
try {
const config = await this.configService.getConfiguration();
return config.sketchDirUri;
} catch (err) {
this.logger.error(`Failed to determine the sketch directory: ${err}`);
return super.getWorkspaceURIFromCli();
}
}
}