mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-11 03:09:29 +00:00
Make tab width 2 spaces (#445)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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.'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user