mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-10 18:59:28 +00:00
Use eslint&prettier for code linting&formatting
This commit is contained in:
committed by
Francesco Stasi
parent
2a3873a923
commit
0592199858
@@ -4,7 +4,10 @@ import { spawn, ChildProcess } from 'child_process';
|
||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { environment } from '@theia/application-package/lib/environment';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
@@ -15,11 +18,12 @@ import { CLI_CONFIG } from './cli-config';
|
||||
import { getExecPath, spawnCommand } from './exec-util';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContribution {
|
||||
|
||||
export class ArduinoDaemonImpl
|
||||
implements ArduinoDaemon, BackendApplicationContribution
|
||||
{
|
||||
@inject(ILogger)
|
||||
@named('daemon')
|
||||
protected readonly logger: ILogger
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariablesServer: EnvVariablesServer;
|
||||
@@ -54,12 +58,20 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
|
||||
this.onData(`Starting daemon from ${cliPath}...`);
|
||||
const daemon = await this.spawnDaemonProcess();
|
||||
// Watchdog process for terminating the daemon process when the backend app terminates.
|
||||
spawn(process.execPath, [join(__dirname, 'daemon-watcher.js'), String(process.pid), String(daemon.pid)], {
|
||||
env: environment.electron.runAsNodeEnv(),
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true
|
||||
}).unref();
|
||||
spawn(
|
||||
process.execPath,
|
||||
[
|
||||
join(__dirname, 'daemon-watcher.js'),
|
||||
String(process.pid),
|
||||
String(daemon.pid),
|
||||
],
|
||||
{
|
||||
env: environment.electron.runAsNodeEnv(),
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
}
|
||||
).unref();
|
||||
|
||||
this.toDispose.pushAll([
|
||||
Disposable.create(() => daemon.kill()),
|
||||
@@ -73,7 +85,7 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
|
||||
let i = 5; // TODO: make this better
|
||||
while (i) {
|
||||
this.onData(`Restarting daemon in ${i} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
i--;
|
||||
}
|
||||
this.onData('Restarting daemon now...');
|
||||
@@ -101,20 +113,29 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
|
||||
if (this._execPath) {
|
||||
return this._execPath;
|
||||
}
|
||||
this._execPath = await getExecPath('arduino-cli', this.onError.bind(this));
|
||||
this._execPath = await getExecPath(
|
||||
'arduino-cli',
|
||||
this.onError.bind(this)
|
||||
);
|
||||
return this._execPath;
|
||||
}
|
||||
|
||||
async getVersion(): Promise<Readonly<{ version: string, commit: string, status?: string }>> {
|
||||
async getVersion(): Promise<
|
||||
Readonly<{ version: string; commit: string; status?: string }>
|
||||
> {
|
||||
const execPath = await this.getExecPath();
|
||||
const raw = await spawnCommand(`"${execPath}"`, ['version', '--format', 'json'], this.onError.bind(this));
|
||||
const raw = await spawnCommand(
|
||||
`"${execPath}"`,
|
||||
['version', '--format', 'json'],
|
||||
this.onError.bind(this)
|
||||
);
|
||||
try {
|
||||
// The CLI `Info` object: https://github.com/arduino/arduino-cli/blob/17d24eb901b1fdaa5a4ec7da3417e9e460f84007/version/version.go#L31-L34
|
||||
const { VersionString, Commit, Status } = JSON.parse(raw);
|
||||
return {
|
||||
version: VersionString,
|
||||
commit: Commit,
|
||||
status: Status
|
||||
status: Status,
|
||||
};
|
||||
} catch {
|
||||
return { version: raw, commit: raw };
|
||||
@@ -124,20 +145,30 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
|
||||
protected async getSpawnArgs(): Promise<string[]> {
|
||||
const configDirUri = await this.envVariablesServer.getConfigDirUri();
|
||||
const cliConfigPath = join(FileUri.fsPath(configDirUri), CLI_CONFIG);
|
||||
return ['daemon', '--config-file', `"${cliConfigPath}"`, '-v', '--log-format', 'json'];
|
||||
return [
|
||||
'daemon',
|
||||
'--config-file',
|
||||
`"${cliConfigPath}"`,
|
||||
'-v',
|
||||
'--log-format',
|
||||
'json',
|
||||
];
|
||||
}
|
||||
|
||||
protected async spawnDaemonProcess(): Promise<ChildProcess> {
|
||||
const [cliPath, args] = await Promise.all([this.getExecPath(), this.getSpawnArgs()]);
|
||||
const [cliPath, args] = await Promise.all([
|
||||
this.getExecPath(),
|
||||
this.getSpawnArgs(),
|
||||
]);
|
||||
const ready = new Deferred<ChildProcess>();
|
||||
const options = { shell: true };
|
||||
const daemon = spawn(`"${cliPath}"`, args, options);
|
||||
|
||||
// If the process exists right after the daemon gRPC server has started (due to an invalid port, unknown address, TCP port in use, etc.)
|
||||
// If the process exists right after the daemon gRPC server has started (due to an invalid port, unknown address, TCP port in use, etc.)
|
||||
// we have no idea about the root cause unless we sniff into the first data package and dispatch the logic on that. Note, we get a exit code 1.
|
||||
let grpcServerIsReady = false;
|
||||
|
||||
daemon.stdout.on('data', data => {
|
||||
daemon.stdout.on('data', (data) => {
|
||||
const message = data.toString();
|
||||
this.onData(message);
|
||||
if (!grpcServerIsReady) {
|
||||
@@ -145,7 +176,10 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
|
||||
if (error) {
|
||||
ready.reject(error);
|
||||
}
|
||||
for (const expected of ['Daemon is listening on TCP port', 'Daemon is now listening on 127.0.0.1']) {
|
||||
for (const expected of [
|
||||
'Daemon is listening on TCP port',
|
||||
'Daemon is now listening on 127.0.0.1',
|
||||
]) {
|
||||
if (message.includes(expected)) {
|
||||
grpcServerIsReady = true;
|
||||
ready.resolve(daemon);
|
||||
@@ -153,7 +187,7 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
|
||||
}
|
||||
}
|
||||
});
|
||||
daemon.stderr.on('data', data => {
|
||||
daemon.stderr.on('data', (data) => {
|
||||
const message = data.toString();
|
||||
this.onData(data.toString());
|
||||
const error = DaemonError.parse(message);
|
||||
@@ -163,10 +197,16 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
|
||||
if (code === 0 || signal === 'SIGINT' || signal === 'SIGKILL') {
|
||||
this.onData('Daemon has stopped.');
|
||||
} else {
|
||||
this.onData(`Daemon exited with ${typeof code === 'undefined' ? `signal '${signal}'` : `exit code: ${code}`}.`);
|
||||
this.onData(
|
||||
`Daemon exited with ${
|
||||
typeof code === 'undefined'
|
||||
? `signal '${signal}'`
|
||||
: `exit code: ${code}`
|
||||
}.`
|
||||
);
|
||||
}
|
||||
});
|
||||
daemon.on('error', error => {
|
||||
daemon.on('error', (error) => {
|
||||
this.onError(error);
|
||||
ready.reject(error);
|
||||
});
|
||||
@@ -198,20 +238,20 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
|
||||
protected onError(error: any): void {
|
||||
this.logger.error(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class DaemonError extends Error {
|
||||
|
||||
constructor(message: string, public readonly code: number, public readonly details?: string) {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly code: number,
|
||||
public readonly details?: string
|
||||
) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, DaemonError.prototype);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace DaemonError {
|
||||
|
||||
export const ADDRESS_IN_USE = 0;
|
||||
export const UNKNOWN_ADDRESS = 2;
|
||||
export const INVALID_PORT = 4;
|
||||
@@ -220,22 +260,44 @@ export namespace DaemonError {
|
||||
export function parse(log: string): DaemonError | undefined {
|
||||
const raw = log.toLocaleLowerCase();
|
||||
if (raw.includes('failed to listen')) {
|
||||
if (raw.includes('address already in use') || (raw.includes('bind')) && raw.includes('only one usage of each socket address')) {
|
||||
return new DaemonError('Failed to listen on TCP port. Address already in use.', DaemonError.ADDRESS_IN_USE);
|
||||
if (
|
||||
raw.includes('address already in use') ||
|
||||
(raw.includes('bind') &&
|
||||
raw.includes('only one usage of each socket address'))
|
||||
) {
|
||||
return new DaemonError(
|
||||
'Failed to listen on TCP port. Address already in use.',
|
||||
DaemonError.ADDRESS_IN_USE
|
||||
);
|
||||
}
|
||||
if (raw.includes('is unknown name') || (raw.includes('tcp/') && (raw.includes('is an invalid port')))) {
|
||||
return new DaemonError('Failed to listen on TCP port. Unknown address.', DaemonError.UNKNOWN_ADDRESS);
|
||||
if (
|
||||
raw.includes('is unknown name') ||
|
||||
(raw.includes('tcp/') && raw.includes('is an invalid port'))
|
||||
) {
|
||||
return new DaemonError(
|
||||
'Failed to listen on TCP port. Unknown address.',
|
||||
DaemonError.UNKNOWN_ADDRESS
|
||||
);
|
||||
}
|
||||
if (raw.includes('is an invalid port')) {
|
||||
return new DaemonError('Failed to listen on TCP port. Invalid port.', DaemonError.INVALID_PORT);
|
||||
return new DaemonError(
|
||||
'Failed to listen on TCP port. Invalid port.',
|
||||
DaemonError.INVALID_PORT
|
||||
);
|
||||
}
|
||||
}
|
||||
// Based on the CLI logging: `failed to serve`, and any other FATAL errors.
|
||||
// https://github.com/arduino/arduino-cli/blob/11abbee8a9f027d087d4230f266a87217677d423/cli/daemon/daemon.go#L89-L94
|
||||
if (raw.includes('failed to serve') && (raw.includes('"fatal"') || raw.includes('fata'))) {
|
||||
return new DaemonError('Unexpected CLI start error.', DaemonError.UNKNOWN, log);
|
||||
if (
|
||||
raw.includes('failed to serve') &&
|
||||
(raw.includes('"fatal"') || raw.includes('fata'))
|
||||
) {
|
||||
return new DaemonError(
|
||||
'Unexpected CLI start error.',
|
||||
DaemonError.UNKNOWN,
|
||||
log
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { ArduinoDaemonImpl } from './arduino-daemon-impl';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { BackendApplicationContribution, BackendApplication as TheiaBackendApplication } from '@theia/core/lib/node/backend-application';
|
||||
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
|
||||
import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service';
|
||||
import {
|
||||
BackendApplicationContribution,
|
||||
BackendApplication as TheiaBackendApplication,
|
||||
} from '@theia/core/lib/node/backend-application';
|
||||
import {
|
||||
LibraryService,
|
||||
LibraryServicePath,
|
||||
} from '../common/protocol/library-service';
|
||||
import {
|
||||
BoardsService,
|
||||
BoardsServicePath,
|
||||
} from '../common/protocol/boards-service';
|
||||
import { LibraryServiceImpl } from './library-service-server-impl';
|
||||
import { BoardsServiceImpl } from './boards-service-impl';
|
||||
import { CoreServiceImpl } from './core-service-impl';
|
||||
@@ -14,24 +23,53 @@ import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core';
|
||||
import { DefaultWorkspaceServer } from './theia/workspace/default-workspace-server';
|
||||
import { WorkspaceServer as TheiaWorkspaceServer } from '@theia/workspace/lib/common';
|
||||
import { SketchesServiceImpl } from './sketches-service-impl';
|
||||
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
|
||||
import { ConfigService, ConfigServicePath } from '../common/protocol/config-service';
|
||||
import { ArduinoDaemon, ArduinoDaemonPath } from '../common/protocol/arduino-daemon';
|
||||
import {
|
||||
SketchesService,
|
||||
SketchesServicePath,
|
||||
} from '../common/protocol/sketches-service';
|
||||
import {
|
||||
ConfigService,
|
||||
ConfigServicePath,
|
||||
} from '../common/protocol/config-service';
|
||||
import {
|
||||
ArduinoDaemon,
|
||||
ArduinoDaemonPath,
|
||||
} from '../common/protocol/arduino-daemon';
|
||||
import { MonitorServiceImpl } from './monitor/monitor-service-impl';
|
||||
import { MonitorService, MonitorServicePath, MonitorServiceClient } from '../common/protocol/monitor-service';
|
||||
import {
|
||||
MonitorService,
|
||||
MonitorServicePath,
|
||||
MonitorServiceClient,
|
||||
} from '../common/protocol/monitor-service';
|
||||
import { MonitorClientProvider } from './monitor/monitor-client-provider';
|
||||
import { ConfigServiceImpl } from './config-service-impl';
|
||||
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
|
||||
import { NodeFileSystemExt } from './node-filesystem-ext';
|
||||
import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext';
|
||||
import {
|
||||
FileSystemExt,
|
||||
FileSystemExtPath,
|
||||
} from '../common/protocol/filesystem-ext';
|
||||
import { ExamplesServiceImpl } from './examples-service-impl';
|
||||
import { ExamplesService, ExamplesServicePath } from '../common/protocol/examples-service';
|
||||
import { ExecutableService, ExecutableServicePath } from '../common/protocol/executable-service';
|
||||
import {
|
||||
ExamplesService,
|
||||
ExamplesServicePath,
|
||||
} from '../common/protocol/examples-service';
|
||||
import {
|
||||
ExecutableService,
|
||||
ExecutableServicePath,
|
||||
} from '../common/protocol/executable-service';
|
||||
import { ExecutableServiceImpl } from './executable-service-impl';
|
||||
import { ResponseServicePath, ResponseService } from '../common/protocol/response-service';
|
||||
import {
|
||||
ResponseServicePath,
|
||||
ResponseService,
|
||||
} from '../common/protocol/response-service';
|
||||
import { NotificationServiceServerImpl } from './notification-service-server';
|
||||
import { NotificationServiceServer, NotificationServiceClient, NotificationServicePath } from '../common/protocol';
|
||||
import {
|
||||
NotificationServiceServer,
|
||||
NotificationServiceClient,
|
||||
NotificationServicePath,
|
||||
} from '../common/protocol';
|
||||
import { BackendApplication } from './theia/core/backend-application';
|
||||
import { BoardDiscovery } from './board-discovery';
|
||||
import { DefaultGitInit } from './theia/git/git-init';
|
||||
@@ -46,44 +84,78 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ConfigService).toService(ConfigServiceImpl);
|
||||
// Note: The config service must start earlier than the daemon, hence the binding order of the BA contribution does matter.
|
||||
bind(BackendApplicationContribution).toService(ConfigServiceImpl);
|
||||
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(ConfigServicePath, () => context.container.get(ConfigService))).inSingletonScope();
|
||||
bind(ConnectionHandler)
|
||||
.toDynamicValue(
|
||||
(context) =>
|
||||
new JsonRpcConnectionHandler(ConfigServicePath, () =>
|
||||
context.container.get(ConfigService)
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
// Shared daemon
|
||||
// Shared daemon
|
||||
bind(ArduinoDaemonImpl).toSelf().inSingletonScope();
|
||||
bind(ArduinoDaemon).toService(ArduinoDaemonImpl);
|
||||
bind(BackendApplicationContribution).toService(ArduinoDaemonImpl);
|
||||
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(ArduinoDaemonPath, () => context.container.get(ArduinoDaemon))).inSingletonScope();
|
||||
bind(ConnectionHandler)
|
||||
.toDynamicValue(
|
||||
(context) =>
|
||||
new JsonRpcConnectionHandler(ArduinoDaemonPath, () =>
|
||||
context.container.get(ArduinoDaemon)
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
// Examples service. One per backend, each connected FE gets a proxy.
|
||||
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(ExamplesServiceImpl).toSelf().inSingletonScope();
|
||||
bind(ExamplesService).toService(ExamplesServiceImpl);
|
||||
bindBackendService(ExamplesServicePath, ExamplesService);
|
||||
}));
|
||||
bind(ConnectionContainerModule).toConstantValue(
|
||||
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(ExamplesServiceImpl).toSelf().inSingletonScope();
|
||||
bind(ExamplesService).toService(ExamplesServiceImpl);
|
||||
bindBackendService(ExamplesServicePath, ExamplesService);
|
||||
})
|
||||
);
|
||||
|
||||
// Exposes the executable paths/URIs to the frontend
|
||||
bind(ExecutableServiceImpl).toSelf().inSingletonScope();
|
||||
bind(ExecutableService).toService(ExecutableServiceImpl);
|
||||
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(ExecutableServicePath, () => context.container.get(ExecutableService))).inSingletonScope();
|
||||
bind(ConnectionHandler)
|
||||
.toDynamicValue(
|
||||
(context) =>
|
||||
new JsonRpcConnectionHandler(ExecutableServicePath, () =>
|
||||
context.container.get(ExecutableService)
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
// Library service. Singleton per backend, each connected FE gets its proxy.
|
||||
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(LibraryServiceImpl).toSelf().inSingletonScope();
|
||||
bind(LibraryService).toService(LibraryServiceImpl);
|
||||
bindBackendService(LibraryServicePath, LibraryService);
|
||||
}));
|
||||
bind(ConnectionContainerModule).toConstantValue(
|
||||
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(LibraryServiceImpl).toSelf().inSingletonScope();
|
||||
bind(LibraryService).toService(LibraryServiceImpl);
|
||||
bindBackendService(LibraryServicePath, LibraryService);
|
||||
})
|
||||
);
|
||||
|
||||
// Shared sketches service
|
||||
bind(SketchesServiceImpl).toSelf().inSingletonScope();
|
||||
bind(SketchesService).toService(SketchesServiceImpl);
|
||||
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(SketchesServicePath, () => context.container.get(SketchesService))).inSingletonScope();
|
||||
bind(ConnectionHandler)
|
||||
.toDynamicValue(
|
||||
(context) =>
|
||||
new JsonRpcConnectionHandler(SketchesServicePath, () =>
|
||||
context.container.get(SketchesService)
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
// Boards service. One instance per connected frontend.
|
||||
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(BoardsServiceImpl).toSelf().inSingletonScope();
|
||||
bind(BoardsService).toService(BoardsServiceImpl);
|
||||
bindBackendService(BoardsServicePath, BoardsService);
|
||||
}));
|
||||
bind(ConnectionContainerModule).toConstantValue(
|
||||
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(BoardsServiceImpl).toSelf().inSingletonScope();
|
||||
bind(BoardsService).toService(BoardsServiceImpl);
|
||||
bindBackendService(BoardsServicePath, BoardsService);
|
||||
})
|
||||
);
|
||||
|
||||
// Shared Arduino core client provider service for the backend.
|
||||
bind(CoreClientProvider).toSelf().inSingletonScope();
|
||||
@@ -92,11 +164,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(BoardDiscovery).toSelf().inSingletonScope();
|
||||
|
||||
// Core service -> `verify` and `upload`. Singleton per BE, each FE connection gets its proxy.
|
||||
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(CoreServiceImpl).toSelf().inSingletonScope();
|
||||
bind(CoreService).toService(CoreServiceImpl);
|
||||
bindBackendService(CoreServicePath, CoreService);
|
||||
}));
|
||||
bind(ConnectionContainerModule).toConstantValue(
|
||||
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(CoreServiceImpl).toSelf().inSingletonScope();
|
||||
bind(CoreService).toService(CoreServiceImpl);
|
||||
bindBackendService(CoreServicePath, CoreService);
|
||||
})
|
||||
);
|
||||
|
||||
// #region Theia customizations
|
||||
|
||||
@@ -109,64 +183,101 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// #endregion Theia customizations
|
||||
|
||||
// Monitor client provider per connected frontend.
|
||||
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(MonitorClientProvider).toSelf().inSingletonScope();
|
||||
bind(MonitorServiceImpl).toSelf().inSingletonScope();
|
||||
bind(MonitorService).toService(MonitorServiceImpl);
|
||||
bindBackendService<MonitorService, MonitorServiceClient>(MonitorServicePath, MonitorService, (service, client) => {
|
||||
service.setClient(client);
|
||||
client.onDidCloseConnection(() => service.dispose());
|
||||
return service;
|
||||
});
|
||||
}));
|
||||
bind(ConnectionContainerModule).toConstantValue(
|
||||
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(MonitorClientProvider).toSelf().inSingletonScope();
|
||||
bind(MonitorServiceImpl).toSelf().inSingletonScope();
|
||||
bind(MonitorService).toService(MonitorServiceImpl);
|
||||
bindBackendService<MonitorService, MonitorServiceClient>(
|
||||
MonitorServicePath,
|
||||
MonitorService,
|
||||
(service, client) => {
|
||||
service.setClient(client);
|
||||
client.onDidCloseConnection(() => service.dispose());
|
||||
return service;
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// File-system extension for mapping paths to URIs
|
||||
bind(NodeFileSystemExt).toSelf().inSingletonScope();
|
||||
bind(FileSystemExt).toService(NodeFileSystemExt);
|
||||
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(FileSystemExtPath, () => context.container.get(FileSystemExt))).inSingletonScope();
|
||||
bind(ConnectionHandler)
|
||||
.toDynamicValue(
|
||||
(context) =>
|
||||
new JsonRpcConnectionHandler(FileSystemExtPath, () =>
|
||||
context.container.get(FileSystemExt)
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
// Output service per connection.
|
||||
bind(ConnectionContainerModule).toConstantValue(ConnectionContainerModule.create(({ bindFrontendService }) => {
|
||||
bindFrontendService(ResponseServicePath, ResponseService);
|
||||
}));
|
||||
bind(ConnectionContainerModule).toConstantValue(
|
||||
ConnectionContainerModule.create(({ bindFrontendService }) => {
|
||||
bindFrontendService(ResponseServicePath, ResponseService);
|
||||
})
|
||||
);
|
||||
|
||||
// Notify all connected frontend instances
|
||||
bind(NotificationServiceServerImpl).toSelf().inSingletonScope();
|
||||
bind(NotificationServiceServer).toService(NotificationServiceServerImpl);
|
||||
bind(ConnectionHandler).toDynamicValue(context =>
|
||||
new JsonRpcConnectionHandler<NotificationServiceClient>(NotificationServicePath, client => {
|
||||
const server = context.container.get<NotificationServiceServer>(NotificationServiceServer);
|
||||
server.setClient(client);
|
||||
client.onDidCloseConnection(() => server.disposeClient(client));
|
||||
return server;
|
||||
})
|
||||
).inSingletonScope();
|
||||
bind(ConnectionHandler)
|
||||
.toDynamicValue(
|
||||
(context) =>
|
||||
new JsonRpcConnectionHandler<NotificationServiceClient>(
|
||||
NotificationServicePath,
|
||||
(client) => {
|
||||
const server =
|
||||
context.container.get<NotificationServiceServer>(
|
||||
NotificationServiceServer
|
||||
);
|
||||
server.setClient(client);
|
||||
client.onDidCloseConnection(() =>
|
||||
server.disposeClient(client)
|
||||
);
|
||||
return server;
|
||||
}
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
// Logger for the Arduino daemon
|
||||
bind(ILogger).toDynamicValue(ctx => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('daemon');
|
||||
}).inSingletonScope().whenTargetNamed('daemon');
|
||||
bind(ILogger)
|
||||
.toDynamicValue((ctx) => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('daemon');
|
||||
})
|
||||
.inSingletonScope()
|
||||
.whenTargetNamed('daemon');
|
||||
|
||||
// Logger for the "serial discovery".
|
||||
bind(ILogger).toDynamicValue(ctx => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('discovery');
|
||||
}).inSingletonScope().whenTargetNamed('discovery');
|
||||
bind(ILogger)
|
||||
.toDynamicValue((ctx) => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('discovery');
|
||||
})
|
||||
.inSingletonScope()
|
||||
.whenTargetNamed('discovery');
|
||||
|
||||
// Logger for the CLI config service. From the CLI config (FS path aware), we make a URI-aware app config.
|
||||
bind(ILogger).toDynamicValue(ctx => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('config');
|
||||
}).inSingletonScope().whenTargetNamed('config');
|
||||
bind(ILogger)
|
||||
.toDynamicValue((ctx) => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('config');
|
||||
})
|
||||
.inSingletonScope()
|
||||
.whenTargetNamed('config');
|
||||
|
||||
// Logger for the monitor service.
|
||||
bind(ILogger).toDynamicValue(ctx => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('monitor-service');
|
||||
}).inSingletonScope().whenTargetNamed('monitor-service');
|
||||
bind(ILogger)
|
||||
.toDynamicValue((ctx) => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('monitor-service');
|
||||
})
|
||||
.inSingletonScope()
|
||||
.whenTargetNamed('monitor-service');
|
||||
|
||||
bind(DefaultGitInit).toSelf();
|
||||
rebind(GitInit).toService(DefaultGitInit);
|
||||
|
||||
});
|
||||
|
||||
@@ -3,8 +3,17 @@ import { ClientDuplexStream } from '@grpc/grpc-js';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import { CoreClientAware } from './core-client-provider';
|
||||
import { BoardListWatchRequest, BoardListWatchResponse } from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
|
||||
import { Board, Port, NotificationServiceServer, AvailablePorts, AttachedBoardsChangeEvent } from '../common/protocol';
|
||||
import {
|
||||
BoardListWatchRequest,
|
||||
BoardListWatchResponse,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
|
||||
import {
|
||||
Board,
|
||||
Port,
|
||||
NotificationServiceServer,
|
||||
AvailablePorts,
|
||||
AttachedBoardsChangeEvent,
|
||||
} from '../common/protocol';
|
||||
|
||||
/**
|
||||
* Singleton service for tracking the available ports and board and broadcasting the
|
||||
@@ -13,7 +22,6 @@ import { Board, Port, NotificationServiceServer, AvailablePorts, AttachedBoardsC
|
||||
*/
|
||||
@injectable()
|
||||
export class BoardDiscovery extends CoreClientAware {
|
||||
|
||||
@inject(ILogger)
|
||||
@named('discovery')
|
||||
protected discoveryLogger: ILogger;
|
||||
@@ -21,7 +29,9 @@ export class BoardDiscovery extends CoreClientAware {
|
||||
@inject(NotificationServiceServer)
|
||||
protected readonly notificationService: NotificationServiceServer;
|
||||
|
||||
protected boardWatchDuplex: ClientDuplexStream<BoardListWatchRequest, BoardListWatchResponse> | undefined;
|
||||
protected boardWatchDuplex:
|
||||
| ClientDuplexStream<BoardListWatchRequest, BoardListWatchResponse>
|
||||
| undefined;
|
||||
|
||||
/**
|
||||
* Keys are the `address` of the ports. \
|
||||
@@ -49,7 +59,6 @@ export class BoardDiscovery extends CoreClientAware {
|
||||
this.boardWatchDuplex.on('data', (resp: BoardListWatchResponse) => {
|
||||
const detectedPort = resp.getPort();
|
||||
if (detectedPort) {
|
||||
|
||||
let eventType: 'add' | 'remove' | 'unknown' = 'unknown';
|
||||
if (resp.getEventType() === 'add') {
|
||||
eventType = 'add';
|
||||
@@ -60,30 +69,46 @@ export class BoardDiscovery extends CoreClientAware {
|
||||
}
|
||||
|
||||
if (eventType === 'unknown') {
|
||||
throw new Error(`Unexpected event type: '${resp.getEventType()}'`);
|
||||
throw new Error(
|
||||
`Unexpected event type: '${resp.getEventType()}'`
|
||||
);
|
||||
}
|
||||
|
||||
const oldState = deepClone(this._state);
|
||||
const newState = deepClone(this._state);
|
||||
|
||||
const address = detectedPort.getAddress();
|
||||
const protocol = Port.Protocol.toProtocol(detectedPort.getProtocol());
|
||||
const protocol = Port.Protocol.toProtocol(
|
||||
detectedPort.getProtocol()
|
||||
);
|
||||
// const label = detectedPort.getProtocolLabel();
|
||||
const port = { address, protocol };
|
||||
const boards: Board[] = [];
|
||||
for (const item of detectedPort.getBoardsList()) {
|
||||
boards.push({ fqbn: item.getFqbn(), name: item.getName() || 'unknown', port });
|
||||
boards.push({
|
||||
fqbn: item.getFqbn(),
|
||||
name: item.getName() || 'unknown',
|
||||
port,
|
||||
});
|
||||
}
|
||||
|
||||
if (eventType === 'add') {
|
||||
if (newState[port.address] !== undefined) {
|
||||
const [, knownBoards] = newState[port.address];
|
||||
console.warn(`Port '${port.address}' was already available. Known boards before override: ${JSON.stringify(knownBoards)}`);
|
||||
console.warn(
|
||||
`Port '${
|
||||
port.address
|
||||
}' was already available. Known boards before override: ${JSON.stringify(
|
||||
knownBoards
|
||||
)}`
|
||||
);
|
||||
}
|
||||
newState[port.address] = [port, boards];
|
||||
} else if (eventType === 'remove') {
|
||||
if (newState[port.address] === undefined) {
|
||||
console.warn(`Port '${port.address}' was not available. Skipping`);
|
||||
console.warn(
|
||||
`Port '${port.address}' was not available. Skipping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
delete newState[port.address];
|
||||
@@ -96,12 +121,12 @@ export class BoardDiscovery extends CoreClientAware {
|
||||
const event: AttachedBoardsChangeEvent = {
|
||||
oldState: {
|
||||
ports: oldAvailablePorts,
|
||||
boards: oldAttachedBoards
|
||||
boards: oldAttachedBoards,
|
||||
},
|
||||
newState: {
|
||||
ports: newAvailablePorts,
|
||||
boards: newAttachedBoards
|
||||
}
|
||||
boards: newAttachedBoards,
|
||||
},
|
||||
};
|
||||
|
||||
this._state = newState;
|
||||
@@ -124,10 +149,9 @@ export class BoardDiscovery extends CoreClientAware {
|
||||
const availablePorts: Port[] = [];
|
||||
for (const address of Object.keys(state)) {
|
||||
// tslint:disable-next-line: whitespace
|
||||
const [port,] = state[address];
|
||||
const [port] = state[address];
|
||||
availablePorts.push(port);
|
||||
}
|
||||
return availablePorts;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,22 +4,46 @@ import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import {
|
||||
BoardsService,
|
||||
Installable,
|
||||
BoardsPackage, Board, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer, ResponseService, NotificationServiceServer, AvailablePorts, BoardWithPackage
|
||||
BoardsPackage,
|
||||
Board,
|
||||
Port,
|
||||
BoardDetails,
|
||||
Tool,
|
||||
ConfigOption,
|
||||
ConfigValue,
|
||||
Programmer,
|
||||
ResponseService,
|
||||
NotificationServiceServer,
|
||||
AvailablePorts,
|
||||
BoardWithPackage,
|
||||
} from '../common/protocol';
|
||||
import {
|
||||
PlatformInstallRequest, PlatformListRequest, PlatformListResponse, PlatformSearchRequest,
|
||||
PlatformSearchResponse, PlatformUninstallRequest
|
||||
PlatformInstallRequest,
|
||||
PlatformListRequest,
|
||||
PlatformListResponse,
|
||||
PlatformSearchRequest,
|
||||
PlatformSearchResponse,
|
||||
PlatformUninstallRequest,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/core_pb';
|
||||
import { Platform } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
|
||||
import { BoardDiscovery } from './board-discovery';
|
||||
import { CoreClientAware } from './core-client-provider';
|
||||
import { BoardDetailsRequest, BoardDetailsResponse, BoardSearchRequest } from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
|
||||
import { ListProgrammersAvailableForUploadRequest, ListProgrammersAvailableForUploadResponse } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
|
||||
import {
|
||||
BoardDetailsRequest,
|
||||
BoardDetailsResponse,
|
||||
BoardSearchRequest,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
|
||||
import {
|
||||
ListProgrammersAvailableForUploadRequest,
|
||||
ListProgrammersAvailableForUploadResponse,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
|
||||
import { InstallWithProgress } from './grpc-installable';
|
||||
|
||||
@injectable()
|
||||
export class BoardsServiceImpl extends CoreClientAware implements BoardsService {
|
||||
|
||||
export class BoardsServiceImpl
|
||||
extends CoreClientAware
|
||||
implements BoardsService
|
||||
{
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@@ -48,32 +72,43 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
return this.boardDiscovery.getAvailablePorts();
|
||||
}
|
||||
|
||||
async getBoardDetails(options: { fqbn: string }): Promise<BoardDetails | undefined> {
|
||||
async getBoardDetails(options: {
|
||||
fqbn: string;
|
||||
}): Promise<BoardDetails | undefined> {
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
const { fqbn } = options;
|
||||
const detailsReq = new BoardDetailsRequest();
|
||||
detailsReq.setInstance(instance);
|
||||
detailsReq.setFqbn(fqbn);
|
||||
const detailsResp = await new Promise<BoardDetailsResponse | undefined>((resolve, reject) => client.boardDetails(detailsReq, (err, resp) => {
|
||||
if (err) {
|
||||
// Required cores are not installed manually: https://github.com/arduino/arduino-cli/issues/954
|
||||
if ((err.message.indexOf('missing platform release') !== -1 && err.message.indexOf('referenced by board') !== -1)
|
||||
// Platform is not installed.
|
||||
|| err.message.indexOf('platform') !== -1 && err.message.indexOf('not installed') !== -1) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
// It's a hack to handle https://github.com/arduino/arduino-cli/issues/1262 gracefully.
|
||||
if (err.message.indexOf('unknown package') !== -1) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(resp);
|
||||
}));
|
||||
const detailsResp = await new Promise<BoardDetailsResponse | undefined>(
|
||||
(resolve, reject) =>
|
||||
client.boardDetails(detailsReq, (err, resp) => {
|
||||
if (err) {
|
||||
// Required cores are not installed manually: https://github.com/arduino/arduino-cli/issues/954
|
||||
if (
|
||||
(err.message.indexOf('missing platform release') !==
|
||||
-1 &&
|
||||
err.message.indexOf('referenced by board') !==
|
||||
-1) ||
|
||||
// Platform is not installed.
|
||||
(err.message.indexOf('platform') !== -1 &&
|
||||
err.message.indexOf('not installed') !== -1)
|
||||
) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
// It's a hack to handle https://github.com/arduino/arduino-cli/issues/1262 gracefully.
|
||||
if (err.message.indexOf('unknown package') !== -1) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(resp);
|
||||
})
|
||||
);
|
||||
|
||||
if (!detailsResp) {
|
||||
return undefined;
|
||||
@@ -81,42 +116,64 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
|
||||
const debuggingSupported = detailsResp.getDebuggingSupported();
|
||||
|
||||
const requiredTools = detailsResp.getToolsDependenciesList().map(t => <Tool>{
|
||||
name: t.getName(),
|
||||
packager: t.getPackager(),
|
||||
version: t.getVersion()
|
||||
});
|
||||
const requiredTools = detailsResp.getToolsDependenciesList().map(
|
||||
(t) =>
|
||||
<Tool>{
|
||||
name: t.getName(),
|
||||
packager: t.getPackager(),
|
||||
version: t.getVersion(),
|
||||
}
|
||||
);
|
||||
|
||||
const configOptions = detailsResp.getConfigOptionsList().map(c => <ConfigOption>{
|
||||
label: c.getOptionLabel(),
|
||||
option: c.getOption(),
|
||||
values: c.getValuesList().map(v => <ConfigValue>{
|
||||
value: v.getValue(),
|
||||
label: v.getValueLabel(),
|
||||
selected: v.getSelected()
|
||||
})
|
||||
});
|
||||
const configOptions = detailsResp.getConfigOptionsList().map(
|
||||
(c) =>
|
||||
<ConfigOption>{
|
||||
label: c.getOptionLabel(),
|
||||
option: c.getOption(),
|
||||
values: c.getValuesList().map(
|
||||
(v) =>
|
||||
<ConfigValue>{
|
||||
value: v.getValue(),
|
||||
label: v.getValueLabel(),
|
||||
selected: v.getSelected(),
|
||||
}
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
const listReq = new ListProgrammersAvailableForUploadRequest();
|
||||
listReq.setInstance(instance);
|
||||
listReq.setFqbn(fqbn);
|
||||
const listResp = await new Promise<ListProgrammersAvailableForUploadResponse>((resolve, reject) => client.listProgrammersAvailableForUpload(listReq, (err, resp) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(resp);
|
||||
}));
|
||||
const listResp =
|
||||
await new Promise<ListProgrammersAvailableForUploadResponse>(
|
||||
(resolve, reject) =>
|
||||
client.listProgrammersAvailableForUpload(
|
||||
listReq,
|
||||
(err, resp) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(resp);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const programmers = listResp.getProgrammersList().map(p => <Programmer>{
|
||||
id: p.getId(),
|
||||
name: p.getName(),
|
||||
platform: p.getPlatform()
|
||||
});
|
||||
const programmers = listResp.getProgrammersList().map(
|
||||
(p) =>
|
||||
<Programmer>{
|
||||
id: p.getId(),
|
||||
name: p.getName(),
|
||||
platform: p.getPlatform(),
|
||||
}
|
||||
);
|
||||
|
||||
let VID = 'N/A';
|
||||
let PID = 'N/A';
|
||||
const usbId = detailsResp.getIdentificationPrefsList().map(item => item.getUsbId()).find(notEmpty);
|
||||
const usbId = detailsResp
|
||||
.getIdentificationPrefsList()
|
||||
.map((item) => item.getUsbId())
|
||||
.find(notEmpty);
|
||||
if (usbId) {
|
||||
VID = usbId.getVid();
|
||||
PID = usbId.getPid();
|
||||
@@ -129,11 +186,13 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
programmers,
|
||||
debuggingSupported,
|
||||
VID,
|
||||
PID
|
||||
PID,
|
||||
};
|
||||
}
|
||||
|
||||
async getBoardPackage(options: { id: string }): Promise<BoardsPackage | undefined> {
|
||||
async getBoardPackage(options: {
|
||||
id: string;
|
||||
}): Promise<BoardsPackage | undefined> {
|
||||
const { id: expectedId } = options;
|
||||
if (!expectedId) {
|
||||
return undefined;
|
||||
@@ -142,41 +201,51 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
return packages.find(({ id }) => id === expectedId);
|
||||
}
|
||||
|
||||
async getContainerBoardPackage(options: { fqbn: string }): Promise<BoardsPackage | undefined> {
|
||||
async getContainerBoardPackage(options: {
|
||||
fqbn: string;
|
||||
}): Promise<BoardsPackage | undefined> {
|
||||
const { fqbn: expectedFqbn } = options;
|
||||
if (!expectedFqbn) {
|
||||
return undefined;
|
||||
}
|
||||
const packages = await this.search({});
|
||||
return packages.find(({ boards }) => boards.some(({ fqbn }) => fqbn === expectedFqbn));
|
||||
return packages.find(({ boards }) =>
|
||||
boards.some(({ fqbn }) => fqbn === expectedFqbn)
|
||||
);
|
||||
}
|
||||
|
||||
async searchBoards({ query }: { query?: string }): Promise<BoardWithPackage[]> {
|
||||
async searchBoards({
|
||||
query,
|
||||
}: {
|
||||
query?: string;
|
||||
}): Promise<BoardWithPackage[]> {
|
||||
const { instance, client } = await this.coreClient();
|
||||
const req = new BoardSearchRequest();
|
||||
req.setSearchArgs(query || '');
|
||||
req.setInstance(instance);
|
||||
const boards = await new Promise<BoardWithPackage[]>((resolve, reject) => {
|
||||
client.boardSearch(req, (error, resp) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
const boards: Array<BoardWithPackage> = [];
|
||||
for (const board of resp.getBoardsList()) {
|
||||
const platform = board.getPlatform();
|
||||
if (platform) {
|
||||
boards.push({
|
||||
name: board.getName(),
|
||||
fqbn: board.getFqbn(),
|
||||
packageId: platform.getId(),
|
||||
packageName: platform.getName()
|
||||
});
|
||||
const boards = await new Promise<BoardWithPackage[]>(
|
||||
(resolve, reject) => {
|
||||
client.boardSearch(req, (error, resp) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
resolve(boards);
|
||||
})
|
||||
});
|
||||
const boards: Array<BoardWithPackage> = [];
|
||||
for (const board of resp.getBoardsList()) {
|
||||
const platform = board.getPlatform();
|
||||
if (platform) {
|
||||
boards.push({
|
||||
name: board.getName(),
|
||||
fqbn: board.getFqbn(),
|
||||
packageId: platform.getId(),
|
||||
packageName: platform.getName(),
|
||||
});
|
||||
}
|
||||
}
|
||||
resolve(boards);
|
||||
});
|
||||
}
|
||||
);
|
||||
return boards;
|
||||
}
|
||||
|
||||
@@ -186,20 +255,31 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
|
||||
const installedPlatformsReq = new PlatformListRequest();
|
||||
installedPlatformsReq.setInstance(instance);
|
||||
const installedPlatformsResp = await new Promise<PlatformListResponse>((resolve, reject) =>
|
||||
client.platformList(installedPlatformsReq, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp))
|
||||
const installedPlatformsResp = await new Promise<PlatformListResponse>(
|
||||
(resolve, reject) =>
|
||||
client.platformList(installedPlatformsReq, (err, resp) =>
|
||||
(!!err ? reject : resolve)(!!err ? err : resp)
|
||||
)
|
||||
);
|
||||
const installedPlatforms = installedPlatformsResp.getInstalledPlatformsList();
|
||||
const installedPlatforms =
|
||||
installedPlatformsResp.getInstalledPlatformsList();
|
||||
|
||||
const req = new PlatformSearchRequest();
|
||||
req.setSearchArgs(options.query || '');
|
||||
req.setAllVersions(true);
|
||||
req.setInstance(instance);
|
||||
const resp = await new Promise<PlatformSearchResponse>((resolve, reject) => client.platformSearch(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)));
|
||||
const resp = await new Promise<PlatformSearchResponse>(
|
||||
(resolve, reject) =>
|
||||
client.platformSearch(req, (err, resp) =>
|
||||
(!!err ? reject : resolve)(!!err ? err : resp)
|
||||
)
|
||||
);
|
||||
const packages = new Map<string, BoardsPackage>();
|
||||
const toPackage = (platform: Platform) => {
|
||||
let installedVersion: string | undefined;
|
||||
const matchingPlatform = installedPlatforms.find(ip => ip.getId() === platform.getId());
|
||||
const matchingPlatform = installedPlatforms.find(
|
||||
(ip) => ip.getId() === platform.getId()
|
||||
);
|
||||
if (!!matchingPlatform) {
|
||||
installedVersion = matchingPlatform.getInstalled();
|
||||
}
|
||||
@@ -208,15 +288,22 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
name: platform.getName(),
|
||||
author: platform.getMaintainer(),
|
||||
availableVersions: [platform.getLatest()],
|
||||
description: platform.getBoardsList().map(b => b.getName()).join(', '),
|
||||
description: platform
|
||||
.getBoardsList()
|
||||
.map((b) => b.getName())
|
||||
.join(', '),
|
||||
installable: true,
|
||||
deprecated: platform.getDeprecated(),
|
||||
summary: 'Boards included in this package:',
|
||||
installedVersion,
|
||||
boards: platform.getBoardsList().map(b => <Board>{ name: b.getName(), fqbn: b.getFqbn() }),
|
||||
moreInfoLink: platform.getWebsite()
|
||||
}
|
||||
}
|
||||
boards: platform
|
||||
.getBoardsList()
|
||||
.map(
|
||||
(b) => <Board>{ name: b.getName(), fqbn: b.getFqbn() }
|
||||
),
|
||||
moreInfoLink: platform.getWebsite(),
|
||||
};
|
||||
};
|
||||
|
||||
// We must group the cores by ID, and sort platforms by, first the installed version, then version alphabetical order.
|
||||
// Otherwise we lose the FQBN information.
|
||||
@@ -229,18 +316,32 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
groupedById.set(id, [platform]);
|
||||
}
|
||||
}
|
||||
const installedAwareVersionComparator = (left: Platform, right: Platform) => {
|
||||
const installedAwareVersionComparator = (
|
||||
left: Platform,
|
||||
right: Platform
|
||||
) => {
|
||||
// XXX: we cannot rely on `platform.getInstalled()`, it is always an empty string.
|
||||
const leftInstalled = !!installedPlatforms.find(ip => ip.getId() === left.getId() && ip.getInstalled() === left.getLatest());
|
||||
const rightInstalled = !!installedPlatforms.find(ip => ip.getId() === right.getId() && ip.getInstalled() === right.getLatest());
|
||||
const leftInstalled = !!installedPlatforms.find(
|
||||
(ip) =>
|
||||
ip.getId() === left.getId() &&
|
||||
ip.getInstalled() === left.getLatest()
|
||||
);
|
||||
const rightInstalled = !!installedPlatforms.find(
|
||||
(ip) =>
|
||||
ip.getId() === right.getId() &&
|
||||
ip.getInstalled() === right.getLatest()
|
||||
);
|
||||
if (leftInstalled && !rightInstalled) {
|
||||
return -1;
|
||||
}
|
||||
if (!leftInstalled && rightInstalled) {
|
||||
return 1;
|
||||
}
|
||||
return Installable.Version.COMPARATOR(left.getLatest(), right.getLatest()); // Higher version comes first.
|
||||
}
|
||||
return Installable.Version.COMPARATOR(
|
||||
left.getLatest(),
|
||||
right.getLatest()
|
||||
); // Higher version comes first.
|
||||
};
|
||||
for (const id of groupedById.keys()) {
|
||||
groupedById.get(id)!.sort(installedAwareVersionComparator);
|
||||
}
|
||||
@@ -251,7 +352,9 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
const pkg = packages.get(id);
|
||||
if (pkg) {
|
||||
pkg.availableVersions.push(platform.getLatest());
|
||||
pkg.availableVersions.sort(Installable.Version.COMPARATOR).reverse();
|
||||
pkg.availableVersions
|
||||
.sort(Installable.Version.COMPARATOR)
|
||||
.reverse();
|
||||
} else {
|
||||
packages.set(id, toPackage(platform));
|
||||
}
|
||||
@@ -261,9 +364,15 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
return [...packages.values()];
|
||||
}
|
||||
|
||||
async install(options: { item: BoardsPackage, progressId?: string, version?: Installable.Version }): Promise<void> {
|
||||
async install(options: {
|
||||
item: BoardsPackage;
|
||||
progressId?: string;
|
||||
version?: Installable.Version;
|
||||
}): Promise<void> {
|
||||
const item = options.item;
|
||||
const version = !!options.version ? options.version : item.availableVersions[0];
|
||||
const version = !!options.version
|
||||
? options.version
|
||||
: item.availableVersions[0];
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
@@ -277,23 +386,37 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
|
||||
console.info('>>> Starting boards package installation...', item);
|
||||
const resp = client.platformInstall(req);
|
||||
resp.on('data', InstallWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService }));
|
||||
resp.on(
|
||||
'data',
|
||||
InstallWithProgress.createDataCallback({
|
||||
progressId: options.progressId,
|
||||
responseService: this.responseService,
|
||||
})
|
||||
);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
resp.on('end', resolve);
|
||||
resp.on('error', error => {
|
||||
this.responseService.appendToOutput({ chunk: `Failed to install platform: ${item.id}.\n` });
|
||||
this.responseService.appendToOutput({ chunk: error.toString() });
|
||||
resp.on('error', (error) => {
|
||||
this.responseService.appendToOutput({
|
||||
chunk: `Failed to install platform: ${item.id}.\n`,
|
||||
});
|
||||
this.responseService.appendToOutput({
|
||||
chunk: error.toString(),
|
||||
});
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
const items = await this.search({});
|
||||
const updated = items.find(other => BoardsPackage.equals(other, item)) || item;
|
||||
const updated =
|
||||
items.find((other) => BoardsPackage.equals(other, item)) || item;
|
||||
this.notificationService.notifyPlatformInstalled({ item: updated });
|
||||
console.info('<<< Boards package installation done.', item);
|
||||
}
|
||||
|
||||
async uninstall(options: { item: BoardsPackage, progressId?: string }): Promise<void> {
|
||||
async uninstall(options: {
|
||||
item: BoardsPackage;
|
||||
progressId?: string;
|
||||
}): Promise<void> {
|
||||
const { item, progressId } = options;
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
@@ -307,7 +430,13 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
|
||||
console.info('>>> Starting boards package uninstallation...', item);
|
||||
const resp = client.platformUninstall(req);
|
||||
resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService }));
|
||||
resp.on(
|
||||
'data',
|
||||
InstallWithProgress.createDataCallback({
|
||||
progressId,
|
||||
responseService: this.responseService,
|
||||
})
|
||||
);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
resp.on('end', resolve);
|
||||
resp.on('error', reject);
|
||||
@@ -317,5 +446,4 @@ export class BoardsServiceImpl extends CoreClientAware implements BoardsService
|
||||
this.notificationService.notifyPlatformUninstalled({ item });
|
||||
console.info('<<< Boards package uninstallation done.', item);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,15 +6,22 @@ export interface BoardManager {
|
||||
readonly additional_urls: Array<string>;
|
||||
}
|
||||
export namespace BoardManager {
|
||||
export function sameAs(left: RecursivePartial<BoardManager> | undefined, right: RecursivePartial<BoardManager> | undefined): boolean {
|
||||
export function sameAs(
|
||||
left: RecursivePartial<BoardManager> | undefined,
|
||||
right: RecursivePartial<BoardManager> | undefined
|
||||
): boolean {
|
||||
const leftOrDefault = left || {};
|
||||
const rightOrDefault = right || {};
|
||||
const leftUrls = Array.from(new Set(leftOrDefault.additional_urls || []));
|
||||
const rightUrls = Array.from(new Set(rightOrDefault.additional_urls || []));
|
||||
const leftUrls = Array.from(
|
||||
new Set(leftOrDefault.additional_urls || [])
|
||||
);
|
||||
const rightUrls = Array.from(
|
||||
new Set(rightOrDefault.additional_urls || [])
|
||||
);
|
||||
if (leftUrls.length !== rightUrls.length) {
|
||||
return false;
|
||||
}
|
||||
return leftUrls.every(url => rightUrls.indexOf(url) !== -1);
|
||||
return leftUrls.every((url) => rightUrls.indexOf(url) !== -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +29,15 @@ export interface Daemon {
|
||||
readonly port: string | number;
|
||||
}
|
||||
export namespace Daemon {
|
||||
export function is(daemon: RecursivePartial<Daemon> | undefined): daemon is Daemon {
|
||||
export function is(
|
||||
daemon: RecursivePartial<Daemon> | undefined
|
||||
): daemon is Daemon {
|
||||
return !!daemon && !!daemon.port;
|
||||
}
|
||||
export function sameAs(left: RecursivePartial<Daemon> | undefined, right: RecursivePartial<Daemon> | undefined): boolean {
|
||||
export function sameAs(
|
||||
left: RecursivePartial<Daemon> | undefined,
|
||||
right: RecursivePartial<Daemon> | undefined
|
||||
): boolean {
|
||||
if (left === undefined) {
|
||||
return right === undefined;
|
||||
}
|
||||
@@ -42,22 +54,31 @@ export interface Directories {
|
||||
readonly user: string;
|
||||
}
|
||||
export namespace Directories {
|
||||
export function is(directories: RecursivePartial<Directories> | undefined): directories is Directories {
|
||||
return !!directories
|
||||
&& !!directories.data
|
||||
&& !!directories.downloads
|
||||
&& !!directories.user;
|
||||
export function is(
|
||||
directories: RecursivePartial<Directories> | undefined
|
||||
): directories is Directories {
|
||||
return (
|
||||
!!directories &&
|
||||
!!directories.data &&
|
||||
!!directories.downloads &&
|
||||
!!directories.user
|
||||
);
|
||||
}
|
||||
export function sameAs(left: RecursivePartial<Directories> | undefined, right: RecursivePartial<Directories> | undefined): boolean {
|
||||
export function sameAs(
|
||||
left: RecursivePartial<Directories> | undefined,
|
||||
right: RecursivePartial<Directories> | undefined
|
||||
): boolean {
|
||||
if (left === undefined) {
|
||||
return right === undefined;
|
||||
}
|
||||
if (right === undefined) {
|
||||
return left === undefined;
|
||||
}
|
||||
return left.data === right.data
|
||||
&& left.downloads === right.downloads
|
||||
&& left.user === right.user;
|
||||
return (
|
||||
left.data === right.data &&
|
||||
left.downloads === right.downloads &&
|
||||
left.user === right.user
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,11 +88,20 @@ export interface Logging {
|
||||
level: Logging.Level;
|
||||
}
|
||||
export namespace Logging {
|
||||
|
||||
export type Format = 'text' | 'json';
|
||||
export type Level = 'trace' | 'debug' | 'info' | 'warning' | 'error' | 'fatal' | 'panic';
|
||||
export type Level =
|
||||
| 'trace'
|
||||
| 'debug'
|
||||
| 'info'
|
||||
| 'warning'
|
||||
| 'error'
|
||||
| 'fatal'
|
||||
| 'panic';
|
||||
|
||||
export function sameAs(left: RecursivePartial<Logging> | undefined, right: RecursivePartial<Logging> | undefined): boolean {
|
||||
export function sameAs(
|
||||
left: RecursivePartial<Logging> | undefined,
|
||||
right: RecursivePartial<Logging> | undefined
|
||||
): boolean {
|
||||
if (left === undefined) {
|
||||
return right === undefined;
|
||||
}
|
||||
@@ -89,7 +119,6 @@ export namespace Logging {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface Network {
|
||||
@@ -110,15 +139,24 @@ export interface DefaultCliConfig extends CliConfig {
|
||||
daemon: Daemon;
|
||||
}
|
||||
export namespace DefaultCliConfig {
|
||||
export function is(config: RecursivePartial<DefaultCliConfig> | undefined): config is DefaultCliConfig {
|
||||
return !!config
|
||||
&& Directories.is(config.directories)
|
||||
&& Daemon.is(config.daemon);
|
||||
export function is(
|
||||
config: RecursivePartial<DefaultCliConfig> | undefined
|
||||
): config is DefaultCliConfig {
|
||||
return (
|
||||
!!config &&
|
||||
Directories.is(config.directories) &&
|
||||
Daemon.is(config.daemon)
|
||||
);
|
||||
}
|
||||
export function sameAs(left: DefaultCliConfig, right: DefaultCliConfig): boolean {
|
||||
return Directories.sameAs(left.directories, right.directories)
|
||||
&& Daemon.sameAs(left.daemon, right.daemon)
|
||||
&& BoardManager.sameAs(left.board_manager, right.board_manager)
|
||||
&& Logging.sameAs(left.logging, right.logging);
|
||||
export function sameAs(
|
||||
left: DefaultCliConfig,
|
||||
right: DefaultCliConfig
|
||||
): boolean {
|
||||
return (
|
||||
Directories.sameAs(left.directories, right.directories) &&
|
||||
Daemon.sameAs(left.daemon, right.daemon) &&
|
||||
BoardManager.sameAs(left.board_manager, right.board_manager) &&
|
||||
Logging.sameAs(left.logging, right.logging)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,17 @@ import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
|
||||
import { ConfigService, Config, NotificationServiceServer, Network } from '../common/protocol';
|
||||
import {
|
||||
ConfigService,
|
||||
Config,
|
||||
NotificationServiceServer,
|
||||
Network,
|
||||
} from '../common/protocol';
|
||||
import { spawnCommand } from './exec-util';
|
||||
import { MergeRequest, WriteRequest } from './cli-protocol/cc/arduino/cli/settings/v1/settings_pb';
|
||||
import {
|
||||
MergeRequest,
|
||||
WriteRequest,
|
||||
} from './cli-protocol/cc/arduino/cli/settings/v1/settings_pb';
|
||||
import { SettingsServiceClient } from './cli-protocol/cc/arduino/cli/settings/v1/settings_grpc_pb';
|
||||
import * as serviceGrpcPb from './cli-protocol/cc/arduino/cli/settings/v1/settings_grpc_pb';
|
||||
import { ArduinoDaemonImpl } from './arduino-daemon-impl';
|
||||
@@ -25,8 +33,9 @@ import { deepClone } from '@theia/core';
|
||||
const track = temp.track();
|
||||
|
||||
@injectable()
|
||||
export class ConfigServiceImpl implements BackendApplicationContribution, ConfigService {
|
||||
|
||||
export class ConfigServiceImpl
|
||||
implements BackendApplicationContribution, ConfigService
|
||||
{
|
||||
@inject(ILogger)
|
||||
@named('config')
|
||||
protected readonly logger: ILogger;
|
||||
@@ -74,20 +83,26 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
|
||||
if (Config.sameAs(this.config, config)) {
|
||||
return;
|
||||
}
|
||||
let copyDefaultCliConfig: DefaultCliConfig | undefined = deepClone(this.cliConfig);
|
||||
let copyDefaultCliConfig: DefaultCliConfig | undefined = deepClone(
|
||||
this.cliConfig
|
||||
);
|
||||
if (!copyDefaultCliConfig) {
|
||||
copyDefaultCliConfig = await this.getFallbackCliConfig();
|
||||
}
|
||||
const { additionalUrls, dataDirUri, downloadsDirUri, sketchDirUri, network } = config;
|
||||
const {
|
||||
additionalUrls,
|
||||
dataDirUri,
|
||||
downloadsDirUri,
|
||||
sketchDirUri,
|
||||
network,
|
||||
} = config;
|
||||
copyDefaultCliConfig.directories = {
|
||||
data: FileUri.fsPath(dataDirUri),
|
||||
downloads: FileUri.fsPath(downloadsDirUri),
|
||||
user: FileUri.fsPath(sketchDirUri)
|
||||
user: FileUri.fsPath(sketchDirUri),
|
||||
};
|
||||
copyDefaultCliConfig.board_manager = {
|
||||
additional_urls: [
|
||||
...additionalUrls
|
||||
]
|
||||
additional_urls: [...additionalUrls],
|
||||
};
|
||||
const proxy = Network.stringify(network);
|
||||
copyDefaultCliConfig.network = { proxy };
|
||||
@@ -108,46 +123,67 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
|
||||
return this.configChangeEmitter.event;
|
||||
}
|
||||
|
||||
async getVersion(): Promise<Readonly<{ version: string, commit: string, status?: string }>> {
|
||||
async getVersion(): Promise<
|
||||
Readonly<{ version: string; commit: string; status?: string }>
|
||||
> {
|
||||
return this.daemon.getVersion();
|
||||
}
|
||||
|
||||
async isInDataDir(uri: string): Promise<boolean> {
|
||||
return this.getConfiguration().then(({ dataDirUri }) => new URI(dataDirUri).isEqualOrParent(new URI(uri)));
|
||||
return this.getConfiguration().then(({ dataDirUri }) =>
|
||||
new URI(dataDirUri).isEqualOrParent(new URI(uri))
|
||||
);
|
||||
}
|
||||
|
||||
async isInSketchDir(uri: string): Promise<boolean> {
|
||||
return this.getConfiguration().then(({ sketchDirUri }) => new URI(sketchDirUri).isEqualOrParent(new URI(uri)));
|
||||
return this.getConfiguration().then(({ sketchDirUri }) =>
|
||||
new URI(sketchDirUri).isEqualOrParent(new URI(uri))
|
||||
);
|
||||
}
|
||||
|
||||
protected async loadCliConfig(): Promise<DefaultCliConfig | undefined> {
|
||||
const cliConfigFileUri = await this.getCliConfigFileUri();
|
||||
const cliConfigPath = FileUri.fsPath(cliConfigFileUri);
|
||||
try {
|
||||
const content = await promisify(fs.readFile)(cliConfigPath, { encoding: 'utf8' });
|
||||
const content = await promisify(fs.readFile)(cliConfigPath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const model = yaml.safeLoad(content) || {};
|
||||
// The CLI can run with partial (missing `port`, `directories`), the app cannot, we merge the default with the user's config.
|
||||
const fallbackModel = await this.getFallbackCliConfig();
|
||||
return deepmerge(fallbackModel, model) as DefaultCliConfig;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error occurred when loading CLI config from ${cliConfigPath}.`, error);
|
||||
this.logger.error(
|
||||
`Error occurred when loading CLI config from ${cliConfigPath}.`,
|
||||
error
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected async getFallbackCliConfig(): Promise<DefaultCliConfig> {
|
||||
const cliPath = await this.daemon.getExecPath();
|
||||
const throwawayDirPath = await new Promise<string>((resolve, reject) => {
|
||||
track.mkdir({}, (err, dirPath) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(dirPath);
|
||||
});
|
||||
});
|
||||
await spawnCommand(`"${cliPath}"`, ['config', 'init', '--dest-dir', `"${throwawayDirPath}"`]);
|
||||
const rawYaml = await promisify(fs.readFile)(path.join(throwawayDirPath, CLI_CONFIG), { encoding: 'utf-8' });
|
||||
const throwawayDirPath = await new Promise<string>(
|
||||
(resolve, reject) => {
|
||||
track.mkdir({}, (err, dirPath) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(dirPath);
|
||||
});
|
||||
}
|
||||
);
|
||||
await spawnCommand(`"${cliPath}"`, [
|
||||
'config',
|
||||
'init',
|
||||
'--dest-dir',
|
||||
`"${throwawayDirPath}"`,
|
||||
]);
|
||||
const rawYaml = await promisify(fs.readFile)(
|
||||
path.join(throwawayDirPath, CLI_CONFIG),
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
const model = yaml.safeLoad(rawYaml.trim());
|
||||
return model as DefaultCliConfig;
|
||||
}
|
||||
@@ -160,22 +196,36 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
|
||||
await this.initCliConfigTo(path.dirname(cliConfigPath));
|
||||
exists = await promisify(fs.exists)(cliConfigPath);
|
||||
if (!exists) {
|
||||
throw new Error(`Could not initialize the default CLI configuration file at ${cliConfigPath}.`);
|
||||
throw new Error(
|
||||
`Could not initialize the default CLI configuration file at ${cliConfigPath}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async initCliConfigTo(fsPathToDir: string): Promise<void> {
|
||||
const cliPath = await this.daemon.getExecPath();
|
||||
await spawnCommand(`"${cliPath}"`, ['config', 'init', '--dest-dir', `"${fsPathToDir}"`]);
|
||||
await spawnCommand(`"${cliPath}"`, [
|
||||
'config',
|
||||
'init',
|
||||
'--dest-dir',
|
||||
`"${fsPathToDir}"`,
|
||||
]);
|
||||
}
|
||||
|
||||
protected async mapCliConfigToAppConfig(cliConfig: DefaultCliConfig): Promise<Config> {
|
||||
protected async mapCliConfigToAppConfig(
|
||||
cliConfig: DefaultCliConfig
|
||||
): Promise<Config> {
|
||||
const { directories } = cliConfig;
|
||||
const { data, user, downloads } = directories;
|
||||
const additionalUrls: Array<string> = [];
|
||||
if (cliConfig.board_manager && cliConfig.board_manager.additional_urls) {
|
||||
additionalUrls.push(...Array.from(new Set(cliConfig.board_manager.additional_urls)));
|
||||
if (
|
||||
cliConfig.board_manager &&
|
||||
cliConfig.board_manager.additional_urls
|
||||
) {
|
||||
additionalUrls.push(
|
||||
...Array.from(new Set(cliConfig.board_manager.additional_urls))
|
||||
);
|
||||
}
|
||||
const network = Network.parse(cliConfig.network?.proxy);
|
||||
return {
|
||||
@@ -183,7 +233,7 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
|
||||
sketchDirUri: FileUri.create(user).toString(),
|
||||
downloadsDirUri: FileUri.create(downloads).toString(),
|
||||
additionalUrls,
|
||||
network
|
||||
network,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,14 +246,17 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
|
||||
this.notificationService.notifyConfigChanged({ config: undefined });
|
||||
}
|
||||
|
||||
protected async updateDaemon(port: string | number, config: DefaultCliConfig): Promise<void> {
|
||||
protected async updateDaemon(
|
||||
port: string | number,
|
||||
config: DefaultCliConfig
|
||||
): Promise<void> {
|
||||
const client = this.createClient(port);
|
||||
const req = new MergeRequest();
|
||||
const json = JSON.stringify(config, null, 2);
|
||||
req.setJsonData(json);
|
||||
console.log(`Updating daemon with 'data': ${json}`);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.merge(req, error => {
|
||||
client.merge(req, (error) => {
|
||||
try {
|
||||
if (error) {
|
||||
reject(error);
|
||||
@@ -224,7 +277,7 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
|
||||
const cliConfigPath = FileUri.fsPath(cliConfigUri);
|
||||
req.setFilePath(cliConfigPath);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.write(req, error => {
|
||||
client.write(req, (error) => {
|
||||
try {
|
||||
if (error) {
|
||||
reject(error);
|
||||
@@ -240,9 +293,14 @@ export class ConfigServiceImpl implements BackendApplicationContribution, Config
|
||||
|
||||
private createClient(port: string | number): SettingsServiceClient {
|
||||
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
|
||||
// @ts-ignore
|
||||
const SettingsServiceClient = grpc.makeClientConstructor(serviceGrpcPb['cc.arduino.cli.settings.v1.SettingsService'], 'SettingsServiceService') as any;
|
||||
return new SettingsServiceClient(`localhost:${port}`, grpc.credentials.createInsecure()) as SettingsServiceClient;
|
||||
const SettingsServiceClient = grpc.makeClientConstructor(
|
||||
// @ts-expect-error: ignore
|
||||
serviceGrpcPb['cc.arduino.cli.settings.v1.SettingsService'],
|
||||
'SettingsServiceService'
|
||||
) as any;
|
||||
return new SettingsServiceClient(
|
||||
`localhost:${port}`,
|
||||
grpc.credentials.createInsecure()
|
||||
) as SettingsServiceClient;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,13 +5,19 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { GrpcClientProvider } from './grpc-client-provider';
|
||||
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
|
||||
import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
|
||||
import { InitRequest, InitResponse, UpdateIndexRequest, UpdateIndexResponse, UpdateLibrariesIndexRequest, UpdateLibrariesIndexResponse } from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
|
||||
import {
|
||||
InitRequest,
|
||||
InitResponse,
|
||||
UpdateIndexRequest,
|
||||
UpdateIndexResponse,
|
||||
UpdateLibrariesIndexRequest,
|
||||
UpdateLibrariesIndexResponse,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
|
||||
import * as commandsGrpcPb from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
|
||||
import { NotificationServiceServer } from '../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Client> {
|
||||
|
||||
@inject(NotificationServiceServer)
|
||||
protected readonly notificationService: NotificationServiceServer;
|
||||
|
||||
@@ -25,7 +31,9 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
|
||||
client.client.close();
|
||||
}
|
||||
|
||||
protected async reconcileClient(port: string | number | undefined): Promise<void> {
|
||||
protected async reconcileClient(
|
||||
port: string | number | undefined
|
||||
): Promise<void> {
|
||||
if (port && port === this._port) {
|
||||
// No need to create a new gRPC client, but we have to update the indexes.
|
||||
if (this._client && !(this._client instanceof Error)) {
|
||||
@@ -38,31 +46,45 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
|
||||
}
|
||||
}
|
||||
|
||||
protected async createClient(port: string | number): Promise<CoreClientProvider.Client> {
|
||||
protected async createClient(
|
||||
port: string | number
|
||||
): Promise<CoreClientProvider.Client> {
|
||||
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
|
||||
// @ts-ignore
|
||||
const ArduinoCoreServiceClient = grpc.makeClientConstructor(commandsGrpcPb['cc.arduino.cli.commands.v1.ArduinoCoreService'], 'ArduinoCoreServiceService') as any;
|
||||
const client = new ArduinoCoreServiceClient(`localhost:${port}`, grpc.credentials.createInsecure(), this.channelOptions) as ArduinoCoreServiceClient;
|
||||
const ArduinoCoreServiceClient = grpc.makeClientConstructor(
|
||||
// @ts-expect-error: ignore
|
||||
commandsGrpcPb['cc.arduino.cli.commands.v1.ArduinoCoreService'],
|
||||
'ArduinoCoreServiceService'
|
||||
) as any;
|
||||
const client = new ArduinoCoreServiceClient(
|
||||
`localhost:${port}`,
|
||||
grpc.credentials.createInsecure(),
|
||||
this.channelOptions
|
||||
) as ArduinoCoreServiceClient;
|
||||
const initReq = new InitRequest();
|
||||
initReq.setLibraryManagerOnly(false);
|
||||
const initResp = await new Promise<InitResponse>((resolve, reject) => {
|
||||
let resp: InitResponse | undefined = undefined;
|
||||
const stream = client.init(initReq);
|
||||
stream.on('data', (data: InitResponse) => resp = data);
|
||||
stream.on('data', (data: InitResponse) => (resp = data));
|
||||
stream.on('end', () => resolve(resp!));
|
||||
stream.on('error', err => reject(err));
|
||||
stream.on('error', (err) => reject(err));
|
||||
});
|
||||
|
||||
const instance = initResp.getInstance();
|
||||
if (!instance) {
|
||||
throw new Error('Could not retrieve instance from the initialize response.');
|
||||
throw new Error(
|
||||
'Could not retrieve instance from the initialize response.'
|
||||
);
|
||||
}
|
||||
await this.updateIndexes({ instance, client });
|
||||
|
||||
return { instance, client };
|
||||
}
|
||||
|
||||
protected async updateIndexes({ client, instance }: CoreClientProvider.Client): Promise<void> {
|
||||
protected async updateIndexes({
|
||||
client,
|
||||
instance,
|
||||
}: CoreClientProvider.Client): Promise<void> {
|
||||
// in a separate promise, try and update the index
|
||||
let indexUpdateSucceeded = true;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
@@ -75,7 +97,9 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
|
||||
}
|
||||
}
|
||||
if (!indexUpdateSucceeded) {
|
||||
console.error('Could not update the index. Please restart to try again.');
|
||||
console.error(
|
||||
'Could not update the index. Please restart to try again.'
|
||||
);
|
||||
}
|
||||
|
||||
let libIndexUpdateSucceeded = true;
|
||||
@@ -85,11 +109,16 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
|
||||
libIndexUpdateSucceeded = true;
|
||||
break;
|
||||
} catch (e) {
|
||||
console.error(`Error while updating library index in attempt ${i}.`, e);
|
||||
console.error(
|
||||
`Error while updating library index in attempt ${i}.`,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!libIndexUpdateSucceeded) {
|
||||
console.error('Could not update the library index. Please restart to try again.');
|
||||
console.error(
|
||||
'Could not update the library index. Please restart to try again.'
|
||||
);
|
||||
}
|
||||
|
||||
if (indexUpdateSucceeded && libIndexUpdateSucceeded) {
|
||||
@@ -97,7 +126,10 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateLibraryIndex({ client, instance }: CoreClientProvider.Client): Promise<void> {
|
||||
protected async updateLibraryIndex({
|
||||
client,
|
||||
instance,
|
||||
}: CoreClientProvider.Client): Promise<void> {
|
||||
const req = new UpdateLibrariesIndexRequest();
|
||||
req.setInstance(instance);
|
||||
const resp = client.updateLibrariesIndex(req);
|
||||
@@ -116,7 +148,9 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
|
||||
console.log(`Download of '${file}' completed.`);
|
||||
}
|
||||
} else {
|
||||
console.log('The library index has been successfully updated.');
|
||||
console.log(
|
||||
'The library index has been successfully updated.'
|
||||
);
|
||||
}
|
||||
file = undefined;
|
||||
}
|
||||
@@ -128,7 +162,10 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
|
||||
});
|
||||
}
|
||||
|
||||
protected async updateIndex({ client, instance }: CoreClientProvider.Client): Promise<void> {
|
||||
protected async updateIndex({
|
||||
client,
|
||||
instance,
|
||||
}: CoreClientProvider.Client): Promise<void> {
|
||||
const updateReq = new UpdateIndexRequest();
|
||||
updateReq.setInstance(instance);
|
||||
const updateResp = client.updateIndex(updateReq);
|
||||
@@ -158,7 +195,6 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
|
||||
updateResp.on('end', resolve);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
export namespace CoreClientProvider {
|
||||
export interface Client {
|
||||
@@ -169,34 +205,36 @@ export namespace CoreClientProvider {
|
||||
|
||||
@injectable()
|
||||
export abstract class CoreClientAware {
|
||||
|
||||
@inject(CoreClientProvider)
|
||||
protected readonly coreClientProvider: CoreClientProvider;
|
||||
|
||||
protected async coreClient(): Promise<CoreClientProvider.Client> {
|
||||
const coreClient = await new Promise<CoreClientProvider.Client>(async (resolve, reject) => {
|
||||
const handle = (c: CoreClientProvider.Client | Error) => {
|
||||
if (c instanceof Error) {
|
||||
reject(c);
|
||||
} else {
|
||||
resolve(c);
|
||||
}
|
||||
}
|
||||
const client = await this.coreClientProvider.client();
|
||||
if (client) {
|
||||
handle(client);
|
||||
return;
|
||||
}
|
||||
const toDispose = new DisposableCollection();
|
||||
toDispose.push(this.coreClientProvider.onClientReady(async () => {
|
||||
const coreClient = await new Promise<CoreClientProvider.Client>(
|
||||
async (resolve, reject) => {
|
||||
const handle = (c: CoreClientProvider.Client | Error) => {
|
||||
if (c instanceof Error) {
|
||||
reject(c);
|
||||
} else {
|
||||
resolve(c);
|
||||
}
|
||||
};
|
||||
const client = await this.coreClientProvider.client();
|
||||
if (client) {
|
||||
handle(client);
|
||||
return;
|
||||
}
|
||||
toDispose.dispose();
|
||||
}));
|
||||
});
|
||||
const toDispose = new DisposableCollection();
|
||||
toDispose.push(
|
||||
this.coreClientProvider.onClientReady(async () => {
|
||||
const client = await this.coreClientProvider.client();
|
||||
if (client) {
|
||||
handle(client);
|
||||
}
|
||||
toDispose.dispose();
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
return coreClient;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,9 +5,19 @@ import * as jspb from 'google-protobuf';
|
||||
import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb';
|
||||
import { ClientReadableStream } from '@grpc/grpc-js';
|
||||
import { CompilerWarnings, CoreService } from '../common/protocol/core-service';
|
||||
import { CompileRequest, CompileResponse } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb';
|
||||
import {
|
||||
CompileRequest,
|
||||
CompileResponse,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb';
|
||||
import { CoreClientAware } from './core-client-provider';
|
||||
import { BurnBootloaderRequest, BurnBootloaderResponse, UploadRequest, UploadResponse, UploadUsingProgrammerRequest, UploadUsingProgrammerResponse } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
|
||||
import {
|
||||
BurnBootloaderRequest,
|
||||
BurnBootloaderResponse,
|
||||
UploadRequest,
|
||||
UploadResponse,
|
||||
UploadUsingProgrammerRequest,
|
||||
UploadUsingProgrammerResponse,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
|
||||
import { ResponseService } from '../common/protocol/response-service';
|
||||
import { NotificationServiceServer } from '../common/protocol';
|
||||
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
|
||||
@@ -15,14 +25,18 @@ import { firstToUpperCase, firstToLowerCase } from '../common/utils';
|
||||
|
||||
@injectable()
|
||||
export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
|
||||
@inject(ResponseService)
|
||||
protected readonly responseService: ResponseService;
|
||||
|
||||
@inject(NotificationServiceServer)
|
||||
protected readonly notificationService: NotificationServiceServer;
|
||||
|
||||
async compile(options: CoreService.Compile.Options & { exportBinaries?: boolean, compilerWarnings?: CompilerWarnings }): Promise<void> {
|
||||
async compile(
|
||||
options: CoreService.Compile.Options & {
|
||||
exportBinaries?: boolean;
|
||||
compilerWarnings?: CompilerWarnings;
|
||||
}
|
||||
): Promise<void> {
|
||||
const { sketchUri, fqbn, compilerWarnings } = options;
|
||||
const sketchPath = FileUri.fsPath(sketchUri);
|
||||
|
||||
@@ -53,34 +67,59 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
result.on('data', (cr: CompileResponse) => {
|
||||
this.responseService.appendToOutput({ chunk: Buffer.from(cr.getOutStream_asU8()).toString() });
|
||||
this.responseService.appendToOutput({ chunk: Buffer.from(cr.getErrStream_asU8()).toString() });
|
||||
this.responseService.appendToOutput({
|
||||
chunk: Buffer.from(cr.getOutStream_asU8()).toString(),
|
||||
});
|
||||
this.responseService.appendToOutput({
|
||||
chunk: Buffer.from(cr.getErrStream_asU8()).toString(),
|
||||
});
|
||||
});
|
||||
result.on('error', error => reject(error));
|
||||
result.on('error', (error) => reject(error));
|
||||
result.on('end', () => resolve());
|
||||
});
|
||||
this.responseService.appendToOutput({ chunk: '\n--------------------------\nCompilation complete.\n' });
|
||||
this.responseService.appendToOutput({
|
||||
chunk: '\n--------------------------\nCompilation complete.\n',
|
||||
});
|
||||
} catch (e) {
|
||||
this.responseService.appendToOutput({ chunk: `Compilation error: ${e}\n`, severity: 'error' });
|
||||
this.responseService.appendToOutput({
|
||||
chunk: `Compilation error: ${e}\n`,
|
||||
severity: 'error',
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async upload(options: CoreService.Upload.Options): Promise<void> {
|
||||
await this.doUpload(options, () => new UploadRequest(), (client, req) => client.upload(req));
|
||||
await this.doUpload(
|
||||
options,
|
||||
() => new UploadRequest(),
|
||||
(client, req) => client.upload(req)
|
||||
);
|
||||
}
|
||||
|
||||
async uploadUsingProgrammer(options: CoreService.Upload.Options): Promise<void> {
|
||||
await this.doUpload(options, () => new UploadUsingProgrammerRequest(), (client, req) => client.uploadUsingProgrammer(req), 'upload using programmer');
|
||||
async uploadUsingProgrammer(
|
||||
options: CoreService.Upload.Options
|
||||
): Promise<void> {
|
||||
await this.doUpload(
|
||||
options,
|
||||
() => new UploadUsingProgrammerRequest(),
|
||||
(client, req) => client.uploadUsingProgrammer(req),
|
||||
'upload using programmer'
|
||||
);
|
||||
}
|
||||
|
||||
protected async doUpload(
|
||||
options: CoreService.Upload.Options,
|
||||
requestProvider: () => UploadRequest | UploadUsingProgrammerRequest,
|
||||
// tslint:disable-next-line:max-line-length
|
||||
responseHandler: (client: ArduinoCoreServiceClient, req: UploadRequest | UploadUsingProgrammerRequest) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
|
||||
task: string = 'upload'): Promise<void> {
|
||||
|
||||
responseHandler: (
|
||||
client: ArduinoCoreServiceClient,
|
||||
req: UploadRequest | UploadUsingProgrammerRequest
|
||||
) => ClientReadableStream<
|
||||
UploadResponse | UploadUsingProgrammerResponse
|
||||
>,
|
||||
task = 'upload'
|
||||
): Promise<void> {
|
||||
await this.compile(Object.assign(options, { exportBinaries: false }));
|
||||
const { sketchUri, fqbn, port, programmer } = options;
|
||||
const sketchPath = FileUri.fsPath(sketchUri);
|
||||
@@ -107,20 +146,34 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
result.on('data', (resp: UploadResponse) => {
|
||||
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() });
|
||||
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() });
|
||||
this.responseService.appendToOutput({
|
||||
chunk: Buffer.from(resp.getOutStream_asU8()).toString(),
|
||||
});
|
||||
this.responseService.appendToOutput({
|
||||
chunk: Buffer.from(resp.getErrStream_asU8()).toString(),
|
||||
});
|
||||
});
|
||||
result.on('error', error => reject(error));
|
||||
result.on('error', (error) => reject(error));
|
||||
result.on('end', () => resolve());
|
||||
});
|
||||
this.responseService.appendToOutput({ chunk: '\n--------------------------\n' + firstToLowerCase(task) + ' complete.\n' });
|
||||
this.responseService.appendToOutput({
|
||||
chunk:
|
||||
'\n--------------------------\n' +
|
||||
firstToLowerCase(task) +
|
||||
' complete.\n',
|
||||
});
|
||||
} catch (e) {
|
||||
this.responseService.appendToOutput({ chunk: `${firstToUpperCase(task)} error: ${e}\n`, severity: 'error' });
|
||||
this.responseService.appendToOutput({
|
||||
chunk: `${firstToUpperCase(task)} error: ${e}\n`,
|
||||
severity: 'error',
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
|
||||
async burnBootloader(
|
||||
options: CoreService.Bootloader.Options
|
||||
): Promise<void> {
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
const { fqbn, port, programmer } = options;
|
||||
@@ -141,19 +194,29 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
result.on('data', (resp: BurnBootloaderResponse) => {
|
||||
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getOutStream_asU8()).toString() });
|
||||
this.responseService.appendToOutput({ chunk: Buffer.from(resp.getErrStream_asU8()).toString() });
|
||||
this.responseService.appendToOutput({
|
||||
chunk: Buffer.from(resp.getOutStream_asU8()).toString(),
|
||||
});
|
||||
this.responseService.appendToOutput({
|
||||
chunk: Buffer.from(resp.getErrStream_asU8()).toString(),
|
||||
});
|
||||
});
|
||||
result.on('error', error => reject(error));
|
||||
result.on('error', (error) => reject(error));
|
||||
result.on('end', () => resolve());
|
||||
});
|
||||
} catch (e) {
|
||||
this.responseService.appendToOutput({ chunk: `Error while burning the bootloader: ${e}\n`, severity: 'error' });
|
||||
this.responseService.appendToOutput({
|
||||
chunk: `Error while burning the bootloader: ${e}\n`,
|
||||
severity: 'error',
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private mergeSourceOverrides(req: { getSourceOverrideMap(): jspb.Map<string, string> }, options: CoreService.Compile.Options): void {
|
||||
private mergeSourceOverrides(
|
||||
req: { getSourceOverrideMap(): jspb.Map<string, string> },
|
||||
options: CoreService.Compile.Options
|
||||
): void {
|
||||
const sketchPath = FileUri.fsPath(options.sketchUri);
|
||||
for (const uri of Object.keys(options.sourceOverride)) {
|
||||
const content = options.sourceOverride[uri];
|
||||
@@ -163,5 +226,4 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface DaemonLog {
|
||||
}
|
||||
|
||||
export namespace DaemonLog {
|
||||
|
||||
export interface Url {
|
||||
readonly Scheme: string;
|
||||
readonly Host: string;
|
||||
@@ -15,19 +14,19 @@ export namespace DaemonLog {
|
||||
}
|
||||
|
||||
export namespace Url {
|
||||
|
||||
export function is(arg: any | undefined): arg is Url {
|
||||
return !!arg
|
||||
&& typeof arg.Scheme === 'string'
|
||||
&& typeof arg.Host === 'string'
|
||||
&& typeof arg.Path === 'string';
|
||||
return (
|
||||
!!arg &&
|
||||
typeof arg.Scheme === 'string' &&
|
||||
typeof arg.Host === 'string' &&
|
||||
typeof arg.Path === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
export function toString(url: Url): string {
|
||||
const { Scheme, Host, Path } = url;
|
||||
return `${Scheme}://${Host}${Path}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface System {
|
||||
@@ -37,7 +36,7 @@ export namespace DaemonLog {
|
||||
|
||||
export namespace System {
|
||||
export function toString(system: System): string {
|
||||
return `OS: ${system.os}`
|
||||
return `OS: ${system.os}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,36 +46,48 @@ export namespace DaemonLog {
|
||||
}
|
||||
|
||||
export namespace Tool {
|
||||
|
||||
export function is(arg: any | undefined): arg is Tool {
|
||||
return !!arg && typeof arg.version === 'string' && 'systems' in arg;
|
||||
}
|
||||
|
||||
export function toString(tool: Tool): string {
|
||||
const { version, systems } = tool;
|
||||
return `Version: ${version}${!!systems ? ` Systems: [${tool.systems.map(System.toString).join(', ')}]` : ''}`;
|
||||
return `Version: ${version}${
|
||||
!!systems
|
||||
? ` Systems: [${tool.systems
|
||||
.map(System.toString)
|
||||
.join(', ')}]`
|
||||
: ''
|
||||
}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type Level = 'trace' | 'debug' | 'info' | 'warning' | 'error';
|
||||
|
||||
export function is(arg: any | undefined): arg is DaemonLog {
|
||||
return !!arg
|
||||
&& typeof arg.time === 'string'
|
||||
&& typeof arg.level === 'string'
|
||||
&& typeof arg.msg === 'string'
|
||||
return (
|
||||
!!arg &&
|
||||
typeof arg.time === 'string' &&
|
||||
typeof arg.level === 'string' &&
|
||||
typeof arg.msg === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
export function toLogLevel(log: DaemonLog): LogLevel {
|
||||
const { level } = log;
|
||||
switch (level) {
|
||||
case 'trace': return LogLevel.TRACE;
|
||||
case 'debug': return LogLevel.DEBUG;
|
||||
case 'info': return LogLevel.INFO;
|
||||
case 'warning': return LogLevel.WARN;
|
||||
case 'error': return LogLevel.ERROR;
|
||||
default: return LogLevel.INFO;
|
||||
case 'trace':
|
||||
return LogLevel.TRACE;
|
||||
case 'debug':
|
||||
return LogLevel.DEBUG;
|
||||
case 'info':
|
||||
return LogLevel.INFO;
|
||||
case 'warning':
|
||||
return LogLevel.WARN;
|
||||
case 'error':
|
||||
return LogLevel.ERROR;
|
||||
default:
|
||||
return LogLevel.INFO;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,11 +110,13 @@ export namespace DaemonLog {
|
||||
result.push(maybeDaemonLog);
|
||||
continue;
|
||||
}
|
||||
} catch { /* NOOP */ }
|
||||
} catch {
|
||||
/* NOOP */
|
||||
}
|
||||
result.push({
|
||||
time: new Date().toString(),
|
||||
level: 'info',
|
||||
msg: messages[i]
|
||||
msg: messages[i],
|
||||
});
|
||||
}
|
||||
return result;
|
||||
@@ -111,13 +124,21 @@ export namespace DaemonLog {
|
||||
|
||||
export function toPrettyString(logMessages: string): string {
|
||||
const parsed = parse(logMessages);
|
||||
return parsed.map(log => toMessage(log)).join('\n') + '\n';
|
||||
return parsed.map((log) => toMessage(log)).join('\n') + '\n';
|
||||
}
|
||||
|
||||
function toMessage(log: DaemonLog, options: { omitLogLevel: boolean } = { omitLogLevel: false }): string {
|
||||
const details = Object.keys(log).filter(key => key !== 'msg' && key !== 'level' && key !== 'time').map(key => toDetails(log, key)).join(', ');
|
||||
const logLevel = options.omitLogLevel ? '' : `[${log.level.toUpperCase()}] `;
|
||||
return `${logLevel}${log.msg}${!!details ? ` [${details}]` : ''}`
|
||||
function toMessage(
|
||||
log: DaemonLog,
|
||||
options: { omitLogLevel: boolean } = { omitLogLevel: false }
|
||||
): string {
|
||||
const details = Object.keys(log)
|
||||
.filter((key) => key !== 'msg' && key !== 'level' && key !== 'time')
|
||||
.map((key) => toDetails(log, key))
|
||||
.join(', ');
|
||||
const logLevel = options.omitLogLevel
|
||||
? ''
|
||||
: `[${log.level.toUpperCase()}] `;
|
||||
return `${logLevel}${log.msg}${!!details ? ` [${details}]` : ''}`;
|
||||
}
|
||||
|
||||
function toDetails(log: DaemonLog, key: string): string {
|
||||
@@ -131,5 +152,4 @@ export namespace DaemonLog {
|
||||
}
|
||||
return `${key.toLowerCase()}: ${value}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as psTree from 'ps-tree';
|
||||
const kill = require('tree-kill');
|
||||
const [theiaPid, daemonPid] = process.argv.slice(2).map(id => Number.parseInt(id, 10));
|
||||
const [theiaPid, daemonPid] = process.argv
|
||||
.slice(2)
|
||||
.map((id) => Number.parseInt(id, 10));
|
||||
|
||||
setInterval(() => {
|
||||
try {
|
||||
|
||||
@@ -7,12 +7,15 @@ import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import { Sketch, SketchContainer } from '../common/protocol/sketches-service';
|
||||
import { SketchesServiceImpl } from './sketches-service-impl';
|
||||
import { ExamplesService } from '../common/protocol/examples-service';
|
||||
import { LibraryLocation, LibraryPackage, LibraryService } from '../common/protocol';
|
||||
import {
|
||||
LibraryLocation,
|
||||
LibraryPackage,
|
||||
LibraryService,
|
||||
} from '../common/protocol';
|
||||
import { ConfigServiceImpl } from './config-service-impl';
|
||||
|
||||
@injectable()
|
||||
export class ExamplesServiceImpl implements ExamplesService {
|
||||
|
||||
@inject(SketchesServiceImpl)
|
||||
protected readonly sketchesService: SketchesServiceImpl;
|
||||
|
||||
@@ -35,22 +38,35 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
}
|
||||
const exampleRootPath = join(__dirname, '..', '..', 'Examples');
|
||||
const exampleNames = await promisify(fs.readdir)(exampleRootPath);
|
||||
this._all = await Promise.all(exampleNames.map(name => join(exampleRootPath, name)).map(path => this.load(path)));
|
||||
this._all = await Promise.all(
|
||||
exampleNames
|
||||
.map((name) => join(exampleRootPath, name))
|
||||
.map((path) => this.load(path))
|
||||
);
|
||||
return this._all;
|
||||
}
|
||||
|
||||
// TODO: decide whether it makes sense to cache them. Keys should be: `fqbn` + version of containing core/library.
|
||||
async installed({ fqbn }: { fqbn?: string }): Promise<{ user: SketchContainer[], current: SketchContainer[], any: SketchContainer[] }> {
|
||||
async installed({ fqbn }: { fqbn?: string }): Promise<{
|
||||
user: SketchContainer[];
|
||||
current: SketchContainer[];
|
||||
any: SketchContainer[];
|
||||
}> {
|
||||
const user: SketchContainer[] = [];
|
||||
const current: SketchContainer[] = [];
|
||||
const any: SketchContainer[] = [];
|
||||
const packages: LibraryPackage[] = await this.libraryService.list({ fqbn });
|
||||
const packages: LibraryPackage[] = await this.libraryService.list({
|
||||
fqbn,
|
||||
});
|
||||
for (const pkg of packages) {
|
||||
const container = await this.tryGroupExamples(pkg);
|
||||
const { location } = pkg;
|
||||
if (location === LibraryLocation.USER) {
|
||||
user.push(container);
|
||||
} else if (location === LibraryLocation.PLATFORM_BUILTIN || LibraryLocation.REFERENCED_PLATFORM_BUILTIN) {
|
||||
} else if (
|
||||
location === LibraryLocation.PLATFORM_BUILTIN ||
|
||||
LibraryLocation.REFERENCED_PLATFORM_BUILTIN
|
||||
) {
|
||||
current.push(container);
|
||||
} else {
|
||||
any.push(container);
|
||||
@@ -64,25 +80,46 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
* folder hierarchy. This method tries to workaround it by falling back to the `installDirUri` and manually creating the
|
||||
* location of the examples. Otherwise it creates the example container from the direct examples FS paths.
|
||||
*/
|
||||
protected async tryGroupExamples({ label, exampleUris, installDirUri }: LibraryPackage): Promise<SketchContainer> {
|
||||
const paths = exampleUris.map(uri => FileUri.fsPath(uri));
|
||||
protected async tryGroupExamples({
|
||||
label,
|
||||
exampleUris,
|
||||
installDirUri,
|
||||
}: LibraryPackage): Promise<SketchContainer> {
|
||||
const paths = exampleUris.map((uri) => FileUri.fsPath(uri));
|
||||
if (installDirUri) {
|
||||
for (const example of ['example', 'Example', 'EXAMPLE', 'examples', 'Examples', 'EXAMPLES']) {
|
||||
const examplesPath = join(FileUri.fsPath(installDirUri), example);
|
||||
for (const example of [
|
||||
'example',
|
||||
'Example',
|
||||
'EXAMPLE',
|
||||
'examples',
|
||||
'Examples',
|
||||
'EXAMPLES',
|
||||
]) {
|
||||
const examplesPath = join(
|
||||
FileUri.fsPath(installDirUri),
|
||||
example
|
||||
);
|
||||
const exists = await promisify(fs.exists)(examplesPath);
|
||||
const isDir = exists && (await promisify(fs.lstat)(examplesPath)).isDirectory();
|
||||
const isDir =
|
||||
exists &&
|
||||
(await promisify(fs.lstat)(examplesPath)).isDirectory();
|
||||
if (isDir) {
|
||||
const fileNames = await promisify(fs.readdir)(examplesPath);
|
||||
const children: SketchContainer[] = [];
|
||||
const sketches: Sketch[] = [];
|
||||
for (const fileName of fileNames) {
|
||||
const subPath = join(examplesPath, fileName);
|
||||
const subIsDir = (await promisify(fs.lstat)(subPath)).isDirectory();
|
||||
const subIsDir = (
|
||||
await promisify(fs.lstat)(subPath)
|
||||
).isDirectory();
|
||||
if (subIsDir) {
|
||||
const sketch = await this.tryLoadSketch(subPath);
|
||||
if (!sketch) {
|
||||
const container = await this.load(subPath);
|
||||
if (container.children.length || container.sketches.length) {
|
||||
if (
|
||||
container.children.length ||
|
||||
container.sketches.length
|
||||
) {
|
||||
children.push(container);
|
||||
}
|
||||
} else {
|
||||
@@ -93,22 +130,24 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
return {
|
||||
label,
|
||||
children,
|
||||
sketches
|
||||
sketches,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const sketches = await Promise.all(paths.map(path => this.tryLoadSketch(path)));
|
||||
const sketches = await Promise.all(
|
||||
paths.map((path) => this.tryLoadSketch(path))
|
||||
);
|
||||
return {
|
||||
label,
|
||||
children: [],
|
||||
sketches: sketches.filter(notEmpty)
|
||||
sketches: sketches.filter(notEmpty),
|
||||
};
|
||||
}
|
||||
|
||||
// Built-ins are included inside the IDE.
|
||||
protected async load(path: string): Promise<SketchContainer> {
|
||||
if (!await promisify(fs.exists)(path)) {
|
||||
if (!(await promisify(fs.exists)(path))) {
|
||||
throw new Error('Examples are not available');
|
||||
}
|
||||
const stat = await promisify(fs.stat)(path);
|
||||
@@ -118,7 +157,7 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
const names = await promisify(fs.readdir)(path);
|
||||
const sketches: Sketch[] = [];
|
||||
const children: SketchContainer[] = [];
|
||||
for (const p of names.map(name => join(path, name))) {
|
||||
for (const p of names.map((name) => join(path, name))) {
|
||||
const stat = await promisify(fs.stat)(p);
|
||||
if (stat.isDirectory()) {
|
||||
const sketch = await this.tryLoadSketch(p);
|
||||
@@ -134,7 +173,7 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
return {
|
||||
label,
|
||||
children,
|
||||
sketches
|
||||
sketches,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -149,11 +188,12 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
|
||||
protected async tryLoadSketch(path: string): Promise<Sketch | undefined> {
|
||||
try {
|
||||
const sketch = await this.sketchesService.loadSketch(FileUri.create(path).toString());
|
||||
const sketch = await this.sketchesService.loadSketch(
|
||||
FileUri.create(path).toString()
|
||||
);
|
||||
return sketch;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ export async function getExecPath(
|
||||
commandName: string,
|
||||
onError: (error: Error) => void = (error) => console.log(error),
|
||||
versionArg?: string | undefined,
|
||||
inBinDir?: boolean): Promise<string> {
|
||||
|
||||
inBinDir?: boolean
|
||||
): Promise<string> {
|
||||
const execName = `${commandName}${os.platform() === 'win32' ? '.exe' : ''}`;
|
||||
const relativePath = ['..', '..', 'build'];
|
||||
if (inBinDir) {
|
||||
@@ -20,13 +20,23 @@ export async function getExecPath(
|
||||
return buildCommand;
|
||||
}
|
||||
const versionRegexp = /\d+\.\d+\.\d+/;
|
||||
const buildVersion = await spawnCommand(`"${buildCommand}"`, [versionArg], onError);
|
||||
const buildVersion = await spawnCommand(
|
||||
`"${buildCommand}"`,
|
||||
[versionArg],
|
||||
onError
|
||||
);
|
||||
const buildShortVersion = (buildVersion.match(versionRegexp) || [])[0];
|
||||
const pathCommand = await new Promise<string | undefined>(resolve => which(execName, (error, path) => resolve(error ? undefined : path)));
|
||||
const pathCommand = await new Promise<string | undefined>((resolve) =>
|
||||
which(execName, (error, path) => resolve(error ? undefined : path))
|
||||
);
|
||||
if (!pathCommand) {
|
||||
return buildCommand;
|
||||
}
|
||||
const pathVersion = await spawnCommand(`"${pathCommand}"`, [versionArg], onError);
|
||||
const pathVersion = await spawnCommand(
|
||||
`"${pathCommand}"`,
|
||||
[versionArg],
|
||||
onError
|
||||
);
|
||||
const pathShortVersion = (pathVersion.match(versionRegexp) || [])[0];
|
||||
if (semver.gt(pathShortVersion, buildShortVersion)) {
|
||||
return pathCommand;
|
||||
@@ -34,38 +44,52 @@ export async function getExecPath(
|
||||
return buildCommand;
|
||||
}
|
||||
|
||||
export function spawnCommand(command: string, args: string[], onError: (error: Error) => void = (error) => console.log(error)): Promise<string> {
|
||||
export function spawnCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
onError: (error: Error) => void = (error) => console.log(error)
|
||||
): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const cp = spawn(command, args, { windowsHide: true, shell: true });
|
||||
const outBuffers: Buffer[] = [];
|
||||
const errBuffers: Buffer[] = [];
|
||||
cp.stdout.on('data', (b: Buffer) => outBuffers.push(b));
|
||||
cp.stderr.on('data', (b: Buffer) => errBuffers.push(b));
|
||||
cp.on('error', error => {
|
||||
cp.on('error', (error) => {
|
||||
onError(error);
|
||||
reject(error);
|
||||
});
|
||||
cp.on('exit', (code, signal) => {
|
||||
if (code === 0) {
|
||||
const result = Buffer.concat(outBuffers).toString('utf8').trim()
|
||||
const result = Buffer.concat(outBuffers)
|
||||
.toString('utf8')
|
||||
.trim();
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
if (errBuffers.length > 0) {
|
||||
const message = Buffer.concat(errBuffers).toString('utf8').trim();
|
||||
const error = new Error(`Error executing ${command} ${args.join(' ')}: ${message}`);
|
||||
onError(error)
|
||||
const message = Buffer.concat(errBuffers)
|
||||
.toString('utf8')
|
||||
.trim();
|
||||
const error = new Error(
|
||||
`Error executing ${command} ${args.join(' ')}: ${message}`
|
||||
);
|
||||
onError(error);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
if (signal) {
|
||||
const error = new Error(`Process exited with signal: ${signal}`);
|
||||
const error = new Error(
|
||||
`Process exited with signal: ${signal}`
|
||||
);
|
||||
onError(error);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
if (code) {
|
||||
const error = new Error(`Process exited with exit code: ${code}`);
|
||||
const error = new Error(
|
||||
`Process exited with exit code: ${code}`
|
||||
);
|
||||
onError(error);
|
||||
reject(error);
|
||||
return;
|
||||
|
||||
@@ -6,25 +6,27 @@ import { ExecutableService } from '../common/protocol/executable-service';
|
||||
|
||||
@injectable()
|
||||
export class ExecutableServiceImpl implements ExecutableService {
|
||||
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
async list(): Promise<{ clangdUri: string, cliUri: string, lsUri: string }> {
|
||||
async list(): Promise<{
|
||||
clangdUri: string;
|
||||
cliUri: string;
|
||||
lsUri: string;
|
||||
}> {
|
||||
const [ls, clangd, cli] = await Promise.all([
|
||||
getExecPath('arduino-language-server', this.onError.bind(this)),
|
||||
getExecPath('clangd', this.onError.bind(this), undefined, true),
|
||||
getExecPath('arduino-cli', this.onError.bind(this))
|
||||
getExecPath('arduino-cli', this.onError.bind(this)),
|
||||
]);
|
||||
return {
|
||||
clangdUri: FileUri.create(clangd).toString(),
|
||||
cliUri: FileUri.create(cli).toString(),
|
||||
lsUri: FileUri.create(ls).toString()
|
||||
lsUri: FileUri.create(ls).toString(),
|
||||
};
|
||||
}
|
||||
|
||||
protected onError(error: Error): void {
|
||||
this.logger.error(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ArduinoDaemonImpl } from './arduino-daemon-impl';
|
||||
|
||||
@injectable()
|
||||
export abstract class GrpcClientProvider<C> {
|
||||
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@@ -45,7 +44,9 @@ export abstract class GrpcClientProvider<C> {
|
||||
}
|
||||
}
|
||||
|
||||
protected async reconcileClient(port: string | number | undefined): Promise<void> {
|
||||
protected async reconcileClient(
|
||||
port: string | number | undefined
|
||||
): Promise<void> {
|
||||
if (this._port === port) {
|
||||
return; // Nothing to do.
|
||||
}
|
||||
@@ -59,7 +60,7 @@ export abstract class GrpcClientProvider<C> {
|
||||
const client = await this.createClient(this._port);
|
||||
this._client = client;
|
||||
} catch (error) {
|
||||
this.logger.error('Could not create client for gRPC.', error)
|
||||
this.logger.error('Could not create client for gRPC.', error);
|
||||
this._client = error;
|
||||
}
|
||||
}
|
||||
@@ -69,11 +70,10 @@ export abstract class GrpcClientProvider<C> {
|
||||
|
||||
protected abstract close(client: C): void;
|
||||
|
||||
protected get channelOptions(): object {
|
||||
protected get channelOptions(): Record<string, unknown> {
|
||||
return {
|
||||
'grpc.max_send_message_length': 512 * 1024 * 1024,
|
||||
'grpc.max_receive_message_length': 512 * 1024 * 1024
|
||||
'grpc.max_receive_message_length': 512 * 1024 * 1024,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ProgressMessage, ResponseService } from '../common/protocol/response-service';
|
||||
import { DownloadProgress, TaskProgress } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
|
||||
import {
|
||||
ProgressMessage,
|
||||
ResponseService,
|
||||
} from '../common/protocol/response-service';
|
||||
import {
|
||||
DownloadProgress,
|
||||
TaskProgress,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
|
||||
|
||||
export interface InstallResponse {
|
||||
getProgress?(): DownloadProgress | undefined;
|
||||
@@ -7,7 +13,6 @@ export interface InstallResponse {
|
||||
}
|
||||
|
||||
export namespace InstallWithProgress {
|
||||
|
||||
export interface Options {
|
||||
/**
|
||||
* _unknown_ progress if falsy.
|
||||
@@ -16,23 +21,36 @@ export namespace InstallWithProgress {
|
||||
readonly responseService: ResponseService;
|
||||
}
|
||||
|
||||
export function createDataCallback({ responseService, progressId }: InstallWithProgress.Options): (response: InstallResponse) => void {
|
||||
export function createDataCallback({
|
||||
responseService,
|
||||
progressId,
|
||||
}: InstallWithProgress.Options): (response: InstallResponse) => void {
|
||||
let localFile = '';
|
||||
let localTotalSize = Number.NaN;
|
||||
return (response: InstallResponse) => {
|
||||
const download = response.getProgress ? response.getProgress() : undefined;
|
||||
const download = response.getProgress
|
||||
? response.getProgress()
|
||||
: undefined;
|
||||
const task = response.getTaskProgress();
|
||||
if (!download && !task) {
|
||||
throw new Error("Implementation error. Neither 'download' nor 'task' is available.");
|
||||
throw new Error(
|
||||
"Implementation error. Neither 'download' nor 'task' is available."
|
||||
);
|
||||
}
|
||||
if (task && download) {
|
||||
throw new Error("Implementation error. Both 'download' and 'task' are available.");
|
||||
throw new Error(
|
||||
"Implementation error. Both 'download' and 'task' are available."
|
||||
);
|
||||
}
|
||||
if (task) {
|
||||
const message = task.getName() || task.getMessage();
|
||||
if (message) {
|
||||
if (progressId) {
|
||||
responseService.reportProgress({ progressId, message, work: { done: Number.NaN, total: Number.NaN } });
|
||||
responseService.reportProgress({
|
||||
progressId,
|
||||
message,
|
||||
work: { done: Number.NaN, total: Number.NaN },
|
||||
});
|
||||
}
|
||||
responseService.appendToOutput({ chunk: `${message}\n` });
|
||||
}
|
||||
@@ -40,7 +58,10 @@ export namespace InstallWithProgress {
|
||||
if (download.getFile() && !localFile) {
|
||||
localFile = download.getFile();
|
||||
}
|
||||
if (download.getTotalSize() > 0 && Number.isNaN(localTotalSize)) {
|
||||
if (
|
||||
download.getTotalSize() > 0 &&
|
||||
Number.isNaN(localTotalSize)
|
||||
) {
|
||||
localTotalSize = download.getTotalSize();
|
||||
}
|
||||
|
||||
@@ -51,15 +72,29 @@ export namespace InstallWithProgress {
|
||||
|
||||
if (progressId && localFile) {
|
||||
let work: ProgressMessage.Work | undefined = undefined;
|
||||
if (download.getDownloaded() > 0 && !Number.isNaN(localTotalSize)) {
|
||||
work = { total: localTotalSize, done: download.getDownloaded() };
|
||||
if (
|
||||
download.getDownloaded() > 0 &&
|
||||
!Number.isNaN(localTotalSize)
|
||||
) {
|
||||
work = {
|
||||
total: localTotalSize,
|
||||
done: download.getDownloaded(),
|
||||
};
|
||||
}
|
||||
responseService.reportProgress({ progressId, message: `Downloading ${localFile}`, work });
|
||||
responseService.reportProgress({
|
||||
progressId,
|
||||
message: `Downloading ${localFile}`,
|
||||
work,
|
||||
});
|
||||
}
|
||||
if (download.getCompleted()) {
|
||||
// Discard local state.
|
||||
if (progressId && !Number.isNaN(localTotalSize)) {
|
||||
responseService.reportProgress({ progressId, message: '', work: { done: Number.NaN, total: Number.NaN } });
|
||||
responseService.reportProgress({
|
||||
progressId,
|
||||
message: '',
|
||||
work: { done: Number.NaN, total: Number.NaN },
|
||||
});
|
||||
}
|
||||
localFile = '';
|
||||
localTotalSize = Number.NaN;
|
||||
@@ -67,6 +102,4 @@ export namespace InstallWithProgress {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { LibraryDependency, LibraryLocation, LibraryPackage, LibraryService } from '../common/protocol/library-service';
|
||||
import {
|
||||
LibraryDependency,
|
||||
LibraryLocation,
|
||||
LibraryPackage,
|
||||
LibraryService,
|
||||
} from '../common/protocol/library-service';
|
||||
import { CoreClientAware } from './core-client-provider';
|
||||
import {
|
||||
InstalledLibrary, Library, LibraryInstallRequest, LibraryListRequest, LibraryListResponse, LibraryLocation as GrpcLibraryLocation, LibraryRelease,
|
||||
LibraryResolveDependenciesRequest, LibraryUninstallRequest, ZipLibraryInstallRequest, LibrarySearchRequest,
|
||||
LibrarySearchResponse
|
||||
InstalledLibrary,
|
||||
Library,
|
||||
LibraryInstallRequest,
|
||||
LibraryListRequest,
|
||||
LibraryListResponse,
|
||||
LibraryLocation as GrpcLibraryLocation,
|
||||
LibraryRelease,
|
||||
LibraryResolveDependenciesRequest,
|
||||
LibraryUninstallRequest,
|
||||
ZipLibraryInstallRequest,
|
||||
LibrarySearchRequest,
|
||||
LibrarySearchResponse,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/lib_pb';
|
||||
import { Installable } from '../common/protocol/installable';
|
||||
import { ILogger, notEmpty } from '@theia/core';
|
||||
@@ -13,8 +27,10 @@ import { ResponseService, NotificationServiceServer } from '../common/protocol';
|
||||
import { InstallWithProgress } from './grpc-installable';
|
||||
|
||||
@injectable()
|
||||
export class LibraryServiceImpl extends CoreClientAware implements LibraryService {
|
||||
|
||||
export class LibraryServiceImpl
|
||||
extends CoreClientAware
|
||||
implements LibraryService
|
||||
{
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@@ -30,7 +46,12 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
|
||||
const listReq = new LibraryListRequest();
|
||||
listReq.setInstance(instance);
|
||||
const installedLibsResp = await new Promise<LibraryListResponse>((resolve, reject) => client.libraryList(listReq, (err, resp) => !!err ? reject(err) : resolve(resp)));
|
||||
const installedLibsResp = await new Promise<LibraryListResponse>(
|
||||
(resolve, reject) =>
|
||||
client.libraryList(listReq, (err, resp) =>
|
||||
!!err ? reject(err) : resolve(resp)
|
||||
)
|
||||
);
|
||||
const installedLibs = installedLibsResp.getInstalledLibrariesList();
|
||||
const installedLibsIdx = new Map<string, InstalledLibrary>();
|
||||
for (const installedLib of installedLibs) {
|
||||
@@ -45,29 +66,48 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
const req = new LibrarySearchRequest();
|
||||
req.setQuery(options.query || '');
|
||||
req.setInstance(instance);
|
||||
const resp = await new Promise<LibrarySearchResponse>((resolve, reject) => client.librarySearch(req, (err, resp) => !!err ? reject(err) : resolve(resp)));
|
||||
const items = resp.getLibrariesList()
|
||||
.filter(item => !!item.getLatest())
|
||||
const resp = await new Promise<LibrarySearchResponse>(
|
||||
(resolve, reject) =>
|
||||
client.librarySearch(req, (err, resp) =>
|
||||
!!err ? reject(err) : resolve(resp)
|
||||
)
|
||||
);
|
||||
const items = resp
|
||||
.getLibrariesList()
|
||||
.filter((item) => !!item.getLatest())
|
||||
.slice(0, 50)
|
||||
.map(item => {
|
||||
.map((item) => {
|
||||
// TODO: This seems to contain only the latest item instead of all of the items.
|
||||
const availableVersions = item.getReleasesMap().getEntryList().map(([key, _]) => key).sort(Installable.Version.COMPARATOR).reverse();
|
||||
const availableVersions = item
|
||||
.getReleasesMap()
|
||||
.getEntryList()
|
||||
.map(([key, _]) => key)
|
||||
.sort(Installable.Version.COMPARATOR)
|
||||
.reverse();
|
||||
let installedVersion: string | undefined;
|
||||
const installed = installedLibsIdx.get(item.getName());
|
||||
if (installed) {
|
||||
installedVersion = installed.getLibrary()!.getVersion();
|
||||
}
|
||||
return toLibrary({
|
||||
name: item.getName(),
|
||||
installable: true,
|
||||
installedVersion,
|
||||
}, item.getLatest()!, availableVersions)
|
||||
return toLibrary(
|
||||
{
|
||||
name: item.getName(),
|
||||
installable: true,
|
||||
installedVersion,
|
||||
},
|
||||
item.getLatest()!,
|
||||
availableVersions
|
||||
);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
async list({ fqbn }: { fqbn?: string | undefined }): Promise<LibraryPackage[]> {
|
||||
async list({
|
||||
fqbn,
|
||||
}: {
|
||||
fqbn?: string | undefined;
|
||||
}): Promise<LibraryPackage[]> {
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
const req = new LibraryListRequest();
|
||||
@@ -78,96 +118,145 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
req.setFqbn(fqbn);
|
||||
}
|
||||
|
||||
const resp = await new Promise<LibraryListResponse | undefined>((resolve, reject) => {
|
||||
client.libraryList(req, ((error, r) => {
|
||||
if (error) {
|
||||
const { message } = error;
|
||||
// Required core dependency is missing.
|
||||
// https://github.com/arduino/arduino-cli/issues/954
|
||||
if (message.indexOf('missing platform release') !== -1 && message.indexOf('referenced by board') !== -1) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
// The core for the board is not installed, `lib list` cannot be filtered based on FQBN.
|
||||
// https://github.com/arduino/arduino-cli/issues/955
|
||||
if (message.indexOf('platform') !== -1 && message.indexOf('is not installed') !== -1) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
const resp = await new Promise<LibraryListResponse | undefined>(
|
||||
(resolve, reject) => {
|
||||
client.libraryList(req, (error, r) => {
|
||||
if (error) {
|
||||
const { message } = error;
|
||||
// Required core dependency is missing.
|
||||
// https://github.com/arduino/arduino-cli/issues/954
|
||||
if (
|
||||
message.indexOf('missing platform release') !==
|
||||
-1 &&
|
||||
message.indexOf('referenced by board') !== -1
|
||||
) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
// The core for the board is not installed, `lib list` cannot be filtered based on FQBN.
|
||||
// https://github.com/arduino/arduino-cli/issues/955
|
||||
if (
|
||||
message.indexOf('platform') !== -1 &&
|
||||
message.indexOf('is not installed') !== -1
|
||||
) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a hack to handle https://github.com/arduino/arduino-cli/issues/1262 gracefully.
|
||||
if (message.indexOf('unknown package') !== -1) {
|
||||
resolve(undefined);
|
||||
// It's a hack to handle https://github.com/arduino/arduino-cli/issues/1262 gracefully.
|
||||
if (message.indexOf('unknown package') !== -1) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(r);
|
||||
}));
|
||||
});
|
||||
resolve(r);
|
||||
});
|
||||
}
|
||||
);
|
||||
if (!resp) {
|
||||
return [];
|
||||
}
|
||||
return resp.getInstalledLibrariesList().map(item => {
|
||||
const library = item.getLibrary();
|
||||
if (!library) {
|
||||
return undefined;
|
||||
}
|
||||
const installedVersion = library.getVersion();
|
||||
return toLibrary({
|
||||
name: library.getName(),
|
||||
label: library.getRealName(),
|
||||
installedVersion,
|
||||
installable: true,
|
||||
description: library.getSentence(),
|
||||
summary: library.getParagraph(),
|
||||
moreInfoLink: library.getWebsite(),
|
||||
includes: library.getProvidesIncludesList(),
|
||||
location: this.mapLocation(library.getLocation()),
|
||||
installDirUri: FileUri.create(library.getInstallDir()).toString(),
|
||||
exampleUris: library.getExamplesList().map(fsPath => FileUri.create(fsPath).toString())
|
||||
}, library, [library.getVersion()]);
|
||||
}).filter(notEmpty);
|
||||
return resp
|
||||
.getInstalledLibrariesList()
|
||||
.map((item) => {
|
||||
const library = item.getLibrary();
|
||||
if (!library) {
|
||||
return undefined;
|
||||
}
|
||||
const installedVersion = library.getVersion();
|
||||
return toLibrary(
|
||||
{
|
||||
name: library.getName(),
|
||||
label: library.getRealName(),
|
||||
installedVersion,
|
||||
installable: true,
|
||||
description: library.getSentence(),
|
||||
summary: library.getParagraph(),
|
||||
moreInfoLink: library.getWebsite(),
|
||||
includes: library.getProvidesIncludesList(),
|
||||
location: this.mapLocation(library.getLocation()),
|
||||
installDirUri: FileUri.create(
|
||||
library.getInstallDir()
|
||||
).toString(),
|
||||
exampleUris: library
|
||||
.getExamplesList()
|
||||
.map((fsPath) => FileUri.create(fsPath).toString()),
|
||||
},
|
||||
library,
|
||||
[library.getVersion()]
|
||||
);
|
||||
})
|
||||
.filter(notEmpty);
|
||||
}
|
||||
|
||||
private mapLocation(location: GrpcLibraryLocation): LibraryLocation {
|
||||
switch (location) {
|
||||
case GrpcLibraryLocation.LIBRARY_LOCATION_IDE_BUILTIN: return LibraryLocation.IDE_BUILTIN;
|
||||
case GrpcLibraryLocation.LIBRARY_LOCATION_USER: return LibraryLocation.USER;
|
||||
case GrpcLibraryLocation.LIBRARY_LOCATION_PLATFORM_BUILTIN: return LibraryLocation.PLATFORM_BUILTIN;
|
||||
case GrpcLibraryLocation.LIBRARY_LOCATION_REFERENCED_PLATFORM_BUILTIN: return LibraryLocation.REFERENCED_PLATFORM_BUILTIN;
|
||||
default: throw new Error(`Unexpected location ${location}.`);
|
||||
case GrpcLibraryLocation.LIBRARY_LOCATION_IDE_BUILTIN:
|
||||
return LibraryLocation.IDE_BUILTIN;
|
||||
case GrpcLibraryLocation.LIBRARY_LOCATION_USER:
|
||||
return LibraryLocation.USER;
|
||||
case GrpcLibraryLocation.LIBRARY_LOCATION_PLATFORM_BUILTIN:
|
||||
return LibraryLocation.PLATFORM_BUILTIN;
|
||||
case GrpcLibraryLocation.LIBRARY_LOCATION_REFERENCED_PLATFORM_BUILTIN:
|
||||
return LibraryLocation.REFERENCED_PLATFORM_BUILTIN;
|
||||
default:
|
||||
throw new Error(`Unexpected location ${location}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async listDependencies({ item, version, filterSelf }: { item: LibraryPackage, version: Installable.Version, filterSelf?: boolean }): Promise<LibraryDependency[]> {
|
||||
async listDependencies({
|
||||
item,
|
||||
version,
|
||||
filterSelf,
|
||||
}: {
|
||||
item: LibraryPackage;
|
||||
version: Installable.Version;
|
||||
filterSelf?: boolean;
|
||||
}): Promise<LibraryDependency[]> {
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
const req = new LibraryResolveDependenciesRequest();
|
||||
req.setInstance(instance);
|
||||
req.setName(item.name);
|
||||
req.setVersion(version);
|
||||
const dependencies = await new Promise<LibraryDependency[]>((resolve, reject) => {
|
||||
client.libraryResolveDependencies(req, (error, resp) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(resp.getDependenciesList().map(dep => <LibraryDependency>{
|
||||
name: dep.getName(),
|
||||
installedVersion: dep.getVersionInstalled(),
|
||||
requiredVersion: dep.getVersionRequired()
|
||||
}));
|
||||
})
|
||||
});
|
||||
return filterSelf ? dependencies.filter(({ name }) => name !== item.name) : dependencies;
|
||||
const dependencies = await new Promise<LibraryDependency[]>(
|
||||
(resolve, reject) => {
|
||||
client.libraryResolveDependencies(req, (error, resp) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(
|
||||
resp.getDependenciesList().map(
|
||||
(dep) =>
|
||||
<LibraryDependency>{
|
||||
name: dep.getName(),
|
||||
installedVersion: dep.getVersionInstalled(),
|
||||
requiredVersion: dep.getVersionRequired(),
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
return filterSelf
|
||||
? dependencies.filter(({ name }) => name !== item.name)
|
||||
: dependencies;
|
||||
}
|
||||
|
||||
async install(options: { item: LibraryPackage, progressId?: string, version?: Installable.Version, installDependencies?: boolean }): Promise<void> {
|
||||
async install(options: {
|
||||
item: LibraryPackage;
|
||||
progressId?: string;
|
||||
version?: Installable.Version;
|
||||
installDependencies?: boolean;
|
||||
}): Promise<void> {
|
||||
const item = options.item;
|
||||
const version = !!options.version ? options.version : item.availableVersions[0];
|
||||
const version = !!options.version
|
||||
? options.version
|
||||
: item.availableVersions[0];
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
@@ -181,23 +270,44 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
|
||||
console.info('>>> Starting library package installation...', item);
|
||||
const resp = client.libraryInstall(req);
|
||||
resp.on('data', InstallWithProgress.createDataCallback({ progressId: options.progressId, responseService: this.responseService }));
|
||||
resp.on(
|
||||
'data',
|
||||
InstallWithProgress.createDataCallback({
|
||||
progressId: options.progressId,
|
||||
responseService: this.responseService,
|
||||
})
|
||||
);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
resp.on('end', resolve);
|
||||
resp.on('error', error => {
|
||||
this.responseService.appendToOutput({ chunk: `Failed to install library: ${item.name}${version ? `:${version}` : ''}.\n` });
|
||||
this.responseService.appendToOutput({ chunk: error.toString() });
|
||||
resp.on('error', (error) => {
|
||||
this.responseService.appendToOutput({
|
||||
chunk: `Failed to install library: ${item.name}${
|
||||
version ? `:${version}` : ''
|
||||
}.\n`,
|
||||
});
|
||||
this.responseService.appendToOutput({
|
||||
chunk: error.toString(),
|
||||
});
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
const items = await this.search({});
|
||||
const updated = items.find(other => LibraryPackage.equals(other, item)) || item;
|
||||
const updated =
|
||||
items.find((other) => LibraryPackage.equals(other, item)) || item;
|
||||
this.notificationServer.notifyLibraryInstalled({ item: updated });
|
||||
console.info('<<< Library package installation done.', item);
|
||||
}
|
||||
|
||||
async installZip({ zipUri, progressId, overwrite }: { zipUri: string, progressId?: string, overwrite?: boolean }): Promise<void> {
|
||||
async installZip({
|
||||
zipUri,
|
||||
progressId,
|
||||
overwrite,
|
||||
}: {
|
||||
zipUri: string;
|
||||
progressId?: string;
|
||||
overwrite?: boolean;
|
||||
}): Promise<void> {
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
const req = new ZipLibraryInstallRequest();
|
||||
@@ -207,14 +317,23 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
req.setOverwrite(overwrite);
|
||||
}
|
||||
const resp = client.zipLibraryInstall(req);
|
||||
resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService }));
|
||||
resp.on(
|
||||
'data',
|
||||
InstallWithProgress.createDataCallback({
|
||||
progressId,
|
||||
responseService: this.responseService,
|
||||
})
|
||||
);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
resp.on('end', resolve);
|
||||
resp.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async uninstall(options: { item: LibraryPackage, progressId?: string }): Promise<void> {
|
||||
async uninstall(options: {
|
||||
item: LibraryPackage;
|
||||
progressId?: string;
|
||||
}): Promise<void> {
|
||||
const { item, progressId } = options;
|
||||
const coreClient = await this.coreClient();
|
||||
const { client, instance } = coreClient;
|
||||
@@ -226,7 +345,13 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
|
||||
console.info('>>> Starting library package uninstallation...', item);
|
||||
const resp = client.libraryUninstall(req);
|
||||
resp.on('data', InstallWithProgress.createDataCallback({ progressId, responseService: this.responseService }));
|
||||
resp.on(
|
||||
'data',
|
||||
InstallWithProgress.createDataCallback({
|
||||
progressId,
|
||||
responseService: this.responseService,
|
||||
})
|
||||
);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
resp.on('end', resolve);
|
||||
resp.on('error', reject);
|
||||
@@ -240,10 +365,13 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic
|
||||
this.logger.info('>>> Disposing library service...');
|
||||
this.logger.info('<<< Disposed library service.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function toLibrary(pkg: Partial<LibraryPackage>, lib: LibraryRelease | Library, availableVersions: string[]): LibraryPackage {
|
||||
function toLibrary(
|
||||
pkg: Partial<LibraryPackage>,
|
||||
lib: LibraryRelease | Library,
|
||||
availableVersions: string[]
|
||||
): LibraryPackage {
|
||||
return {
|
||||
name: '',
|
||||
label: '',
|
||||
@@ -258,6 +386,6 @@ function toLibrary(pkg: Partial<LibraryPackage>, lib: LibraryRelease | Library,
|
||||
includes: lib.getProvidesIncludesList(),
|
||||
description: lib.getSentence(),
|
||||
moreInfoLink: lib.getWebsite(),
|
||||
summary: lib.getParagraph()
|
||||
}
|
||||
summary: lib.getParagraph(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,16 +6,21 @@ import { GrpcClientProvider } from '../grpc-client-provider';
|
||||
|
||||
@injectable()
|
||||
export class MonitorClientProvider extends GrpcClientProvider<MonitorServiceClient> {
|
||||
|
||||
createClient(port: string | number): MonitorServiceClient {
|
||||
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
|
||||
// @ts-ignore
|
||||
const MonitorServiceClient = grpc.makeClientConstructor(monitorGrpcPb['cc.arduino.cli.monitor.v1.MonitorService'], 'MonitorServiceService') as any;
|
||||
return new MonitorServiceClient(`localhost:${port}`, grpc.credentials.createInsecure(), this.channelOptions);
|
||||
const MonitorServiceClient = grpc.makeClientConstructor(
|
||||
// @ts-expect-error: ignore
|
||||
monitorGrpcPb['cc.arduino.cli.monitor.v1.MonitorService'],
|
||||
'MonitorServiceService'
|
||||
) as any;
|
||||
return new MonitorServiceClient(
|
||||
`localhost:${port}`,
|
||||
grpc.credentials.createInsecure(),
|
||||
this.channelOptions
|
||||
);
|
||||
}
|
||||
|
||||
close(client: MonitorServiceClient): void {
|
||||
client.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,8 +4,18 @@ import { injectable, inject, named } from 'inversify';
|
||||
import { Struct } from 'google-protobuf/google/protobuf/struct_pb';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { MonitorService, MonitorServiceClient, MonitorConfig, MonitorError, Status } from '../../common/protocol/monitor-service';
|
||||
import { StreamingOpenRequest, StreamingOpenResponse, MonitorConfig as GrpcMonitorConfig } from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_pb';
|
||||
import {
|
||||
MonitorService,
|
||||
MonitorServiceClient,
|
||||
MonitorConfig,
|
||||
MonitorError,
|
||||
Status,
|
||||
} from '../../common/protocol/monitor-service';
|
||||
import {
|
||||
StreamingOpenRequest,
|
||||
StreamingOpenResponse,
|
||||
MonitorConfig as GrpcMonitorConfig,
|
||||
} from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_pb';
|
||||
import { MonitorClientProvider } from './monitor-client-provider';
|
||||
import { Board, Port } from '../../common/protocol/boards-service';
|
||||
|
||||
@@ -13,21 +23,33 @@ interface ErrorWithCode extends Error {
|
||||
readonly code: number;
|
||||
}
|
||||
namespace ErrorWithCode {
|
||||
export function toMonitorError(error: Error, config: MonitorConfig): MonitorError {
|
||||
export function toMonitorError(
|
||||
error: Error,
|
||||
config: MonitorConfig
|
||||
): MonitorError {
|
||||
const { message } = error;
|
||||
let code = undefined;
|
||||
if (is(error)) {
|
||||
// TODO: const `mapping`. Use regex for the `message`.
|
||||
const mapping = new Map<string, number>();
|
||||
mapping.set('1 CANCELLED: Cancelled on client', MonitorError.ErrorCodes.CLIENT_CANCEL);
|
||||
mapping.set('2 UNKNOWN: device not configured', MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED);
|
||||
mapping.set('2 UNKNOWN: error opening serial monitor: Serial port busy', MonitorError.ErrorCodes.DEVICE_BUSY);
|
||||
mapping.set(
|
||||
'1 CANCELLED: Cancelled on client',
|
||||
MonitorError.ErrorCodes.CLIENT_CANCEL
|
||||
);
|
||||
mapping.set(
|
||||
'2 UNKNOWN: device not configured',
|
||||
MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED
|
||||
);
|
||||
mapping.set(
|
||||
'2 UNKNOWN: error opening serial monitor: Serial port busy',
|
||||
MonitorError.ErrorCodes.DEVICE_BUSY
|
||||
);
|
||||
code = mapping.get(message);
|
||||
}
|
||||
return {
|
||||
message,
|
||||
code,
|
||||
config
|
||||
config,
|
||||
};
|
||||
}
|
||||
function is(error: Error & { code?: number }): error is ErrorWithCode {
|
||||
@@ -37,7 +59,6 @@ namespace ErrorWithCode {
|
||||
|
||||
@injectable()
|
||||
export class MonitorServiceImpl implements MonitorService {
|
||||
|
||||
@inject(ILogger)
|
||||
@named('monitor-service')
|
||||
protected readonly logger: ILogger;
|
||||
@@ -46,7 +67,10 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
protected readonly monitorClientProvider: MonitorClientProvider;
|
||||
|
||||
protected client?: MonitorServiceClient;
|
||||
protected connection?: { duplex: ClientDuplexStream<StreamingOpenRequest, StreamingOpenResponse>, config: MonitorConfig };
|
||||
protected connection?: {
|
||||
duplex: ClientDuplexStream<StreamingOpenRequest, StreamingOpenResponse>;
|
||||
config: MonitorConfig;
|
||||
};
|
||||
protected messages: string[] = [];
|
||||
protected onMessageDidReadEmitter = new Emitter<void>();
|
||||
|
||||
@@ -64,7 +88,11 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
}
|
||||
|
||||
async connect(config: MonitorConfig): Promise<Status> {
|
||||
this.logger.info(`>>> Creating serial monitor connection for ${Board.toString(config.board)} on port ${Port.toString(config.port)}...`);
|
||||
this.logger.info(
|
||||
`>>> Creating serial monitor connection for ${Board.toString(
|
||||
config.board
|
||||
)} on port ${Port.toString(config.port)}...`
|
||||
);
|
||||
if (this.connection) {
|
||||
return Status.ALREADY_CONNECTED;
|
||||
}
|
||||
@@ -78,25 +106,37 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
const duplex = client.streamingOpen();
|
||||
this.connection = { duplex, config };
|
||||
|
||||
duplex.on('error', ((error: Error) => {
|
||||
const monitorError = ErrorWithCode.toMonitorError(error, config);
|
||||
this.disconnect(monitorError).then(() => {
|
||||
if (this.client) {
|
||||
this.client.notifyError(monitorError);
|
||||
}
|
||||
if (monitorError.code === undefined) {
|
||||
// Log the original, unexpected error.
|
||||
this.logger.error(error);
|
||||
}
|
||||
});
|
||||
}).bind(this));
|
||||
duplex.on(
|
||||
'error',
|
||||
((error: Error) => {
|
||||
const monitorError = ErrorWithCode.toMonitorError(
|
||||
error,
|
||||
config
|
||||
);
|
||||
this.disconnect(monitorError).then(() => {
|
||||
if (this.client) {
|
||||
this.client.notifyError(monitorError);
|
||||
}
|
||||
if (monitorError.code === undefined) {
|
||||
// Log the original, unexpected error.
|
||||
this.logger.error(error);
|
||||
}
|
||||
});
|
||||
}).bind(this)
|
||||
);
|
||||
|
||||
duplex.on('data', ((resp: StreamingOpenResponse) => {
|
||||
const raw = resp.getData();
|
||||
const message = typeof raw === 'string' ? raw : new TextDecoder('utf8').decode(raw);
|
||||
this.messages.push(message);
|
||||
this.onMessageDidReadEmitter.fire();
|
||||
}).bind(this));
|
||||
duplex.on(
|
||||
'data',
|
||||
((resp: StreamingOpenResponse) => {
|
||||
const raw = resp.getData();
|
||||
const message =
|
||||
typeof raw === 'string'
|
||||
? raw
|
||||
: new TextDecoder('utf8').decode(raw);
|
||||
this.messages.push(message);
|
||||
this.onMessageDidReadEmitter.fire();
|
||||
}).bind(this)
|
||||
);
|
||||
|
||||
const { type, port } = config;
|
||||
const req = new StreamingOpenRequest();
|
||||
@@ -104,14 +144,21 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
monitorConfig.setType(this.mapType(type));
|
||||
monitorConfig.setTarget(port.address);
|
||||
if (config.baudRate !== undefined) {
|
||||
monitorConfig.setAdditionalConfig(Struct.fromJavaScript({ 'BaudRate': config.baudRate }));
|
||||
monitorConfig.setAdditionalConfig(
|
||||
Struct.fromJavaScript({ BaudRate: config.baudRate })
|
||||
);
|
||||
}
|
||||
req.setConfig(monitorConfig);
|
||||
|
||||
return new Promise<Status>(resolve => {
|
||||
return new Promise<Status>((resolve) => {
|
||||
if (this.connection) {
|
||||
this.connection.duplex.write(req, () => {
|
||||
this.logger.info(`<<< Serial monitor connection created for ${Board.toString(config.board, { useFqbn: false })} on port ${Port.toString(config.port)}.`);
|
||||
this.logger.info(
|
||||
`<<< Serial monitor connection created for ${Board.toString(
|
||||
config.board,
|
||||
{ useFqbn: false }
|
||||
)} on port ${Port.toString(config.port)}.`
|
||||
);
|
||||
resolve(Status.OK);
|
||||
});
|
||||
return;
|
||||
@@ -122,7 +169,11 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
|
||||
async disconnect(reason?: MonitorError): Promise<Status> {
|
||||
try {
|
||||
if (!this.connection && reason && reason.code === MonitorError.ErrorCodes.CLIENT_CANCEL) {
|
||||
if (
|
||||
!this.connection &&
|
||||
reason &&
|
||||
reason.code === MonitorError.ErrorCodes.CLIENT_CANCEL
|
||||
) {
|
||||
return Status.OK;
|
||||
}
|
||||
this.logger.info('>>> Disposing monitor connection...');
|
||||
@@ -132,7 +183,12 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
}
|
||||
const { duplex, config } = this.connection;
|
||||
duplex.cancel();
|
||||
this.logger.info(`<<< Disposed monitor connection for ${Board.toString(config.board, { useFqbn: false })} on port ${Port.toString(config.port)}.`);
|
||||
this.logger.info(
|
||||
`<<< Disposed monitor connection for ${Board.toString(
|
||||
config.board,
|
||||
{ useFqbn: false }
|
||||
)} on port ${Port.toString(config.port)}.`
|
||||
);
|
||||
this.connection = undefined;
|
||||
return Status.OK;
|
||||
} finally {
|
||||
@@ -146,7 +202,7 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
}
|
||||
const req = new StreamingOpenRequest();
|
||||
req.setData(new TextEncoder().encode(message));
|
||||
return new Promise<Status>(resolve => {
|
||||
return new Promise<Status>((resolve) => {
|
||||
if (this.connection) {
|
||||
this.connection.duplex.write(req, () => {
|
||||
resolve(Status.OK);
|
||||
@@ -162,7 +218,7 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
if (message) {
|
||||
return { message };
|
||||
}
|
||||
return new Promise<{ message: string }>(resolve => {
|
||||
return new Promise<{ message: string }>((resolve) => {
|
||||
const toDispose = this.onMessageDidReadEmitter.event(() => {
|
||||
toDispose.dispose();
|
||||
resolve(this.request());
|
||||
@@ -170,11 +226,14 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
});
|
||||
}
|
||||
|
||||
protected mapType(type?: MonitorConfig.ConnectionType): GrpcMonitorConfig.TargetType {
|
||||
protected mapType(
|
||||
type?: MonitorConfig.ConnectionType
|
||||
): GrpcMonitorConfig.TargetType {
|
||||
switch (type) {
|
||||
case MonitorConfig.ConnectionType.SERIAL: return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
|
||||
default: return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
|
||||
case MonitorConfig.ConnectionType.SERIAL:
|
||||
return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
|
||||
default:
|
||||
return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import { FileSystemExt } from '../common/protocol/filesystem-ext';
|
||||
|
||||
@injectable()
|
||||
export class NodeFileSystemExt implements FileSystemExt {
|
||||
|
||||
async getUri(fsPath: string): Promise<string> {
|
||||
return FileUri.create(fsPath).toString()
|
||||
return FileUri.create(fsPath).toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,49 +1,66 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { NotificationServiceServer, NotificationServiceClient, AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config, Sketch } from '../common/protocol';
|
||||
import {
|
||||
NotificationServiceServer,
|
||||
NotificationServiceClient,
|
||||
AttachedBoardsChangeEvent,
|
||||
BoardsPackage,
|
||||
LibraryPackage,
|
||||
Config,
|
||||
Sketch,
|
||||
} from '../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class NotificationServiceServerImpl implements NotificationServiceServer {
|
||||
|
||||
export class NotificationServiceServerImpl
|
||||
implements NotificationServiceServer
|
||||
{
|
||||
protected readonly clients: NotificationServiceClient[] = [];
|
||||
|
||||
notifyIndexUpdated(): void {
|
||||
this.clients.forEach(client => client.notifyIndexUpdated());
|
||||
this.clients.forEach((client) => client.notifyIndexUpdated());
|
||||
}
|
||||
|
||||
notifyDaemonStarted(): void {
|
||||
this.clients.forEach(client => client.notifyDaemonStarted());
|
||||
this.clients.forEach((client) => client.notifyDaemonStarted());
|
||||
}
|
||||
|
||||
notifyDaemonStopped(): void {
|
||||
this.clients.forEach(client => client.notifyDaemonStopped());
|
||||
this.clients.forEach((client) => client.notifyDaemonStopped());
|
||||
}
|
||||
|
||||
notifyPlatformInstalled(event: { item: BoardsPackage }): void {
|
||||
this.clients.forEach(client => client.notifyPlatformInstalled(event));
|
||||
this.clients.forEach((client) => client.notifyPlatformInstalled(event));
|
||||
}
|
||||
|
||||
notifyPlatformUninstalled(event: { item: BoardsPackage }): void {
|
||||
this.clients.forEach(client => client.notifyPlatformUninstalled(event));
|
||||
this.clients.forEach((client) =>
|
||||
client.notifyPlatformUninstalled(event)
|
||||
);
|
||||
}
|
||||
|
||||
notifyLibraryInstalled(event: { item: LibraryPackage }): void {
|
||||
this.clients.forEach(client => client.notifyLibraryInstalled(event));
|
||||
this.clients.forEach((client) => client.notifyLibraryInstalled(event));
|
||||
}
|
||||
|
||||
notifyLibraryUninstalled(event: { item: LibraryPackage }): void {
|
||||
this.clients.forEach(client => client.notifyLibraryUninstalled(event));
|
||||
this.clients.forEach((client) =>
|
||||
client.notifyLibraryUninstalled(event)
|
||||
);
|
||||
}
|
||||
|
||||
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
|
||||
this.clients.forEach(client => client.notifyAttachedBoardsChanged(event));
|
||||
this.clients.forEach((client) =>
|
||||
client.notifyAttachedBoardsChanged(event)
|
||||
);
|
||||
}
|
||||
|
||||
notifyConfigChanged(event: { config: Config | undefined }): void {
|
||||
this.clients.forEach(client => client.notifyConfigChanged(event));
|
||||
this.clients.forEach((client) => client.notifyConfigChanged(event));
|
||||
}
|
||||
|
||||
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
|
||||
this.clients.forEach(client => client.notifyRecentSketchesChanged(event));
|
||||
this.clients.forEach((client) =>
|
||||
client.notifyRecentSketchesChanged(event)
|
||||
);
|
||||
}
|
||||
|
||||
setClient(client: NotificationServiceClient): void {
|
||||
@@ -53,7 +70,9 @@ export class NotificationServiceServerImpl implements NotificationServiceServer
|
||||
disposeClient(client: NotificationServiceClient): void {
|
||||
const index = this.clients.indexOf(client);
|
||||
if (index === -1) {
|
||||
console.warn('Could not dispose notification service client. It was not registered.');
|
||||
console.warn(
|
||||
'Could not dispose notification service client. It was not registered.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.clients.splice(index, 1);
|
||||
@@ -65,5 +84,4 @@ export class NotificationServiceServerImpl implements NotificationServiceServer
|
||||
}
|
||||
this.clients.length = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,20 +11,29 @@ import URI from '@theia/core/lib/common/uri';
|
||||
import { FileUri } from '@theia/core/lib/node';
|
||||
import { isWindows } from '@theia/core/lib/common/os';
|
||||
import { ConfigService } from '../common/protocol/config-service';
|
||||
import { SketchesService, Sketch, SketchContainer } from '../common/protocol/sketches-service';
|
||||
import {
|
||||
SketchesService,
|
||||
Sketch,
|
||||
SketchContainer,
|
||||
} from '../common/protocol/sketches-service';
|
||||
import { firstToLowerCase } from '../common/utils';
|
||||
import { NotificationServiceServerImpl } from './notification-service-server';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { CoreClientAware } from './core-client-provider';
|
||||
import { ArchiveSketchRequest, LoadSketchRequest } from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
|
||||
import {
|
||||
ArchiveSketchRequest,
|
||||
LoadSketchRequest,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
|
||||
|
||||
const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/;
|
||||
|
||||
const prefix = '.arduinoIDE-unsaved';
|
||||
|
||||
@injectable()
|
||||
export class SketchesServiceImpl extends CoreClientAware implements SketchesService {
|
||||
|
||||
export class SketchesServiceImpl
|
||||
extends CoreClientAware
|
||||
implements SketchesService
|
||||
{
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@@ -33,13 +42,20 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
|
||||
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
async getSketches({ uri, exclude }: { uri?: string, exclude?: string[] }): Promise<SketchContainerWithDetails> {
|
||||
async getSketches({
|
||||
uri,
|
||||
exclude,
|
||||
}: {
|
||||
uri?: string;
|
||||
exclude?: string[];
|
||||
}): Promise<SketchContainerWithDetails> {
|
||||
const start = Date.now();
|
||||
let sketchbookPath: undefined | string;
|
||||
if (!uri) {
|
||||
const { sketchDirUri } = await this.configService.getConfiguration();
|
||||
const { sketchDirUri } =
|
||||
await this.configService.getConfiguration();
|
||||
sketchbookPath = FileUri.fsPath(sketchDirUri);
|
||||
if (!await promisify(fs.exists)(sketchbookPath)) {
|
||||
if (!(await promisify(fs.exists)(sketchbookPath))) {
|
||||
await promisify(fs.mkdir)(sketchbookPath, { recursive: true });
|
||||
}
|
||||
} else {
|
||||
@@ -48,9 +64,9 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
|
||||
const container: SketchContainerWithDetails = {
|
||||
label: uri ? path.basename(sketchbookPath) : 'Sketchbook',
|
||||
sketches: [],
|
||||
children: []
|
||||
children: [],
|
||||
};
|
||||
if (!await promisify(fs.exists)(sketchbookPath)) {
|
||||
if (!(await promisify(fs.exists)(sketchbookPath))) {
|
||||
return container;
|
||||
}
|
||||
const stat = await promisify(fs.stat)(sketchbookPath);
|
||||
@@ -58,12 +74,18 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
|
||||
return container;
|
||||
}
|
||||
|
||||
const recursivelyLoad = async (fsPath: string, containerToLoad: SketchContainerWithDetails) => {
|
||||
const recursivelyLoad = async (
|
||||
fsPath: string,
|
||||
containerToLoad: SketchContainerWithDetails
|
||||
) => {
|
||||
const filenames = await promisify(fs.readdir)(fsPath);
|
||||
for (const name of filenames) {
|
||||
const childFsPath = path.join(fsPath, name);
|
||||
let skip = false;
|
||||
for (const pattern of exclude || ['**/libraries/**', '**/hardware/**']) {
|
||||
for (const pattern of exclude || [
|
||||
'**/libraries/**',
|
||||
'**/hardware/**',
|
||||
]) {
|
||||
if (!skip && minimatch(childFsPath, pattern)) {
|
||||
skip = true;
|
||||
}
|
||||
@@ -74,17 +96,19 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
|
||||
try {
|
||||
const stat = await promisify(fs.stat)(childFsPath);
|
||||
if (stat.isDirectory()) {
|
||||
const sketch = await this._isSketchFolder(FileUri.create(childFsPath).toString());
|
||||
const sketch = await this._isSketchFolder(
|
||||
FileUri.create(childFsPath).toString()
|
||||
);
|
||||
if (sketch) {
|
||||
containerToLoad.sketches.push({
|
||||
...sketch,
|
||||
mtimeMs: stat.mtimeMs
|
||||
mtimeMs: stat.mtimeMs,
|
||||
});
|
||||
} else {
|
||||
const childContainer: SketchContainerWithDetails = {
|
||||
label: name,
|
||||
children: [],
|
||||
sketches: []
|
||||
sketches: [],
|
||||
};
|
||||
await recursivelyLoad(childFsPath, childContainer);
|
||||
if (!SketchContainer.isEmpty(childContainer)) {
|
||||
@@ -96,13 +120,19 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
|
||||
console.warn(`Could not load sketch from ${childFsPath}.`);
|
||||
}
|
||||
}
|
||||
containerToLoad.sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
||||
containerToLoad.sketches.sort(
|
||||
(left, right) => right.mtimeMs - left.mtimeMs
|
||||
);
|
||||
return containerToLoad;
|
||||
}
|
||||
};
|
||||
|
||||
await recursivelyLoad(sketchbookPath, container);
|
||||
SketchContainer.prune(container);
|
||||
console.debug(`Loading the sketches from ${sketchbookPath} took ${Date.now() - start} ms.`);
|
||||
console.debug(
|
||||
`Loading the sketches from ${sketchbookPath} took ${
|
||||
Date.now() - start
|
||||
} ms.`
|
||||
);
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -111,38 +141,58 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
|
||||
const req = new LoadSketchRequest();
|
||||
req.setSketchPath(FileUri.fsPath(uri));
|
||||
req.setInstance(instance);
|
||||
const sketch = await new Promise<SketchWithDetails>((resolve, reject) => {
|
||||
client.loadSketch(req, async (err, resp) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const sketchFolderPath = resp.getLocationPath();
|
||||
const { mtimeMs } = await promisify(fs.lstat)(sketchFolderPath);
|
||||
resolve({
|
||||
name: path.basename(sketchFolderPath),
|
||||
uri: FileUri.create(sketchFolderPath).toString(),
|
||||
mainFileUri: FileUri.create(resp.getMainFile()).toString(),
|
||||
otherSketchFileUris: resp.getOtherSketchFilesList().map(p => FileUri.create(p).toString()),
|
||||
additionalFileUris: resp.getAdditionalFilesList().map(p => FileUri.create(p).toString()),
|
||||
rootFolderFileUris: resp.getRootFolderFilesList().map(p => FileUri.create(p).toString()),
|
||||
mtimeMs
|
||||
const sketch = await new Promise<SketchWithDetails>(
|
||||
(resolve, reject) => {
|
||||
client.loadSketch(req, async (err, resp) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const sketchFolderPath = resp.getLocationPath();
|
||||
const { mtimeMs } = await promisify(fs.lstat)(
|
||||
sketchFolderPath
|
||||
);
|
||||
resolve({
|
||||
name: path.basename(sketchFolderPath),
|
||||
uri: FileUri.create(sketchFolderPath).toString(),
|
||||
mainFileUri: FileUri.create(
|
||||
resp.getMainFile()
|
||||
).toString(),
|
||||
otherSketchFileUris: resp
|
||||
.getOtherSketchFilesList()
|
||||
.map((p) => FileUri.create(p).toString()),
|
||||
additionalFileUris: resp
|
||||
.getAdditionalFilesList()
|
||||
.map((p) => FileUri.create(p).toString()),
|
||||
rootFolderFileUris: resp
|
||||
.getRootFolderFilesList()
|
||||
.map((p) => FileUri.create(p).toString()),
|
||||
mtimeMs,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
return sketch;
|
||||
}
|
||||
|
||||
private get recentSketchesFsPath(): Promise<string> {
|
||||
return this.envVariableServer.getConfigDirUri().then(uri => path.join(FileUri.fsPath(uri), 'recent-sketches.json'));
|
||||
return this.envVariableServer
|
||||
.getConfigDirUri()
|
||||
.then((uri) =>
|
||||
path.join(FileUri.fsPath(uri), 'recent-sketches.json')
|
||||
);
|
||||
}
|
||||
|
||||
private async loadRecentSketches(fsPath: string): Promise<Record<string, number>> {
|
||||
private async loadRecentSketches(
|
||||
fsPath: string
|
||||
): Promise<Record<string, number>> {
|
||||
let data: Record<string, number> = {};
|
||||
try {
|
||||
const raw = await promisify(fs.readFile)(fsPath, { encoding: 'utf8' });
|
||||
const raw = await promisify(fs.readFile)(fsPath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
data = JSON.parse(raw);
|
||||
} catch { }
|
||||
} catch {}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -178,24 +228,33 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
|
||||
}
|
||||
|
||||
await promisify(fs.writeFile)(fsPath, JSON.stringify(data, null, 2));
|
||||
this.recentlyOpenedSketches().then(sketches => this.notificationService.notifyRecentSketchesChanged({ sketches }));
|
||||
this.recentlyOpenedSketches().then((sketches) =>
|
||||
this.notificationService.notifyRecentSketchesChanged({ sketches })
|
||||
);
|
||||
}
|
||||
|
||||
async recentlyOpenedSketches(): Promise<Sketch[]> {
|
||||
const configDirUri = await this.envVariableServer.getConfigDirUri();
|
||||
const fsPath = path.join(FileUri.fsPath(configDirUri), 'recent-sketches.json');
|
||||
const fsPath = path.join(
|
||||
FileUri.fsPath(configDirUri),
|
||||
'recent-sketches.json'
|
||||
);
|
||||
let data: Record<string, number> = {};
|
||||
try {
|
||||
const raw = await promisify(fs.readFile)(fsPath, { encoding: 'utf8' });
|
||||
const raw = await promisify(fs.readFile)(fsPath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
data = JSON.parse(raw);
|
||||
} catch { }
|
||||
} catch {}
|
||||
|
||||
const sketches: SketchWithDetails[] = []
|
||||
for (const uri of Object.keys(data).sort((left, right) => data[right] - data[left])) {
|
||||
const sketches: SketchWithDetails[] = [];
|
||||
for (const uri of Object.keys(data).sort(
|
||||
(left, right) => data[right] - data[left]
|
||||
)) {
|
||||
try {
|
||||
const sketch = await this.loadSketch(uri);
|
||||
sketches.push(sketch);
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return sketches;
|
||||
@@ -210,15 +269,30 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
|
||||
return;
|
||||
}
|
||||
resolve(dirPath);
|
||||
})
|
||||
});
|
||||
});
|
||||
const destinationUri = FileUri.create(path.join(parentPath, sketch.name)).toString();
|
||||
const destinationUri = FileUri.create(
|
||||
path.join(parentPath, sketch.name)
|
||||
).toString();
|
||||
const copiedSketchUri = await this.copy(sketch, { destinationUri });
|
||||
return this.loadSketch(copiedSketchUri);
|
||||
}
|
||||
|
||||
async createNewSketch(): Promise<Sketch> {
|
||||
const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
||||
const monthNames = [
|
||||
'jan',
|
||||
'feb',
|
||||
'mar',
|
||||
'apr',
|
||||
'may',
|
||||
'jun',
|
||||
'jul',
|
||||
'aug',
|
||||
'sep',
|
||||
'oct',
|
||||
'nov',
|
||||
'dec',
|
||||
];
|
||||
const today = new Date();
|
||||
const parentPath = await new Promise<string>((resolve, reject) => {
|
||||
temp.mkdir({ prefix }, (err, dirPath) => {
|
||||
@@ -229,14 +303,20 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
|
||||
resolve(dirPath);
|
||||
});
|
||||
});
|
||||
const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`;
|
||||
const sketchBaseName = `sketch_${
|
||||
monthNames[today.getMonth()]
|
||||
}${today.getDate()}`;
|
||||
const config = await this.configService.getConfiguration();
|
||||
const user = FileUri.fsPath(config.sketchDirUri);
|
||||
let sketchName: string | undefined;
|
||||
for (let i = 97; i < 97 + 26; i++) {
|
||||
let sketchNameCandidate = `${sketchBaseName}${String.fromCharCode(i)}`;
|
||||
const sketchNameCandidate = `${sketchBaseName}${String.fromCharCode(
|
||||
i
|
||||
)}`;
|
||||
// Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder!
|
||||
if (await promisify(fs.exists)(path.join(user, sketchNameCandidate))) {
|
||||
if (
|
||||
await promisify(fs.exists)(path.join(user, sketchNameCandidate))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -248,10 +328,12 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
|
||||
throw new Error('Cannot create a unique sketch name');
|
||||
}
|
||||
|
||||
const sketchDir = path.join(parentPath, sketchName)
|
||||
const sketchDir = path.join(parentPath, sketchName);
|
||||
const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
|
||||
await promisify(fs.mkdir)(sketchDir, { recursive: true });
|
||||
await promisify(fs.writeFile)(sketchFile, `void setup() {
|
||||
await promisify(fs.writeFile)(
|
||||
sketchFile,
|
||||
`void setup() {
|
||||
// put your setup code here, to run once:
|
||||
|
||||
}
|
||||
@@ -260,7 +342,9 @@ void loop() {
|
||||
// put your main code here, to run repeatedly:
|
||||
|
||||
}
|
||||
`, { encoding: 'utf8' });
|
||||
`,
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
return this.loadSketch(FileUri.create(sketchDir).toString());
|
||||
}
|
||||
|
||||
@@ -284,21 +368,28 @@ void loop() {
|
||||
return !!sketch;
|
||||
}
|
||||
|
||||
private async _isSketchFolder(uri: string): Promise<SketchWithDetails | undefined> {
|
||||
private async _isSketchFolder(
|
||||
uri: string
|
||||
): Promise<SketchWithDetails | undefined> {
|
||||
const fsPath = FileUri.fsPath(uri);
|
||||
let stat: fs.Stats | undefined;
|
||||
try {
|
||||
stat = await promisify(fs.lstat)(fsPath);
|
||||
} catch { }
|
||||
} catch {}
|
||||
if (stat && stat.isDirectory()) {
|
||||
const basename = path.basename(fsPath);
|
||||
const files = await promisify(fs.readdir)(fsPath);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (files[i] === basename + '.ino' || files[i] === basename + '.pde') {
|
||||
if (
|
||||
files[i] === basename + '.ino' ||
|
||||
files[i] === basename + '.pde'
|
||||
) {
|
||||
try {
|
||||
const sketch = await this.loadSketch(FileUri.create(fsPath).toString());
|
||||
const sketch = await this.loadSketch(
|
||||
FileUri.create(fsPath).toString()
|
||||
);
|
||||
return sketch;
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,7 +412,10 @@ void loop() {
|
||||
return sketchPath.indexOf(prefix) !== -1 && sketchPath.startsWith(temp);
|
||||
}
|
||||
|
||||
async copy(sketch: Sketch, { destinationUri }: { destinationUri: string }): Promise<string> {
|
||||
async copy(
|
||||
sketch: Sketch,
|
||||
{ destinationUri }: { destinationUri: string }
|
||||
): Promise<string> {
|
||||
const source = FileUri.fsPath(sketch.uri);
|
||||
const exists = await promisify(fs.exists)(source);
|
||||
if (!exists) {
|
||||
@@ -335,26 +429,34 @@ void loop() {
|
||||
|
||||
const copy = async (sourcePath: string, destinationPath: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
ncp.ncp(sourcePath, destinationPath, async error => {
|
||||
ncp.ncp(sourcePath, destinationPath, async (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
const newName = path.basename(destinationPath);
|
||||
try {
|
||||
const oldPath = path.join(destinationPath, new URI(sketch.mainFileUri).path.base);
|
||||
const newPath = path.join(destinationPath, `${newName}.ino`);
|
||||
const oldPath = path.join(
|
||||
destinationPath,
|
||||
new URI(sketch.mainFileUri).path.base
|
||||
);
|
||||
const newPath = path.join(
|
||||
destinationPath,
|
||||
`${newName}.ino`
|
||||
);
|
||||
if (oldPath !== newPath) {
|
||||
await promisify(fs.rename)(oldPath, newPath);
|
||||
}
|
||||
await this.loadSketch(FileUri.create(destinationPath).toString()); // Sanity check.
|
||||
await this.loadSketch(
|
||||
FileUri.create(destinationPath).toString()
|
||||
); // Sanity check.
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
// https://github.com/arduino/arduino-ide/issues/65
|
||||
// When copying `/path/to/sketchbook/sketch_A` to `/path/to/sketchbook/sketch_A/anything` on a non-POSIX filesystem,
|
||||
// `ncp` makes a recursion and copies the folders over and over again. In such cases, we copy the source into a temp folder,
|
||||
@@ -388,7 +490,7 @@ void loop() {
|
||||
req.setSketchPath(FileUri.fsPath(sketch.uri));
|
||||
req.setArchivePath(archivePath);
|
||||
await new Promise<string>((resolve, reject) => {
|
||||
client.archiveSketch(req, err => {
|
||||
client.archiveSketch(req, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
@@ -407,10 +509,12 @@ void loop() {
|
||||
async getIdeTempFolderPath(sketch: Sketch): Promise<string> {
|
||||
const sketchPath = FileUri.fsPath(sketch.uri);
|
||||
await fs.promises.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible.
|
||||
const suffix = crypto.createHash('md5').update(sketchPath).digest('hex');
|
||||
const suffix = crypto
|
||||
.createHash('md5')
|
||||
.update(sketchPath)
|
||||
.digest('hex');
|
||||
return path.join(os.tmpdir(), `arduino-ide2-${suffix}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface SketchWithDetails extends Sketch {
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import { inject, injectable, named } from 'inversify';
|
||||
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
||||
import { BackendApplication as TheiaBackendApplication, BackendApplicationContribution, BackendApplicationCliContribution } from '@theia/core/lib/node/backend-application';
|
||||
import {
|
||||
BackendApplication as TheiaBackendApplication,
|
||||
BackendApplicationContribution,
|
||||
BackendApplicationCliContribution,
|
||||
} from '@theia/core/lib/node/backend-application';
|
||||
|
||||
@injectable()
|
||||
export class BackendApplication extends TheiaBackendApplication {
|
||||
|
||||
constructor(
|
||||
@inject(ContributionProvider) @named(BackendApplicationContribution) protected readonly contributionsProvider: ContributionProvider<BackendApplicationContribution>,
|
||||
@inject(BackendApplicationCliContribution) protected readonly cliParams: BackendApplicationCliContribution
|
||||
@inject(ContributionProvider)
|
||||
@named(BackendApplicationContribution)
|
||||
protected readonly contributionsProvider: ContributionProvider<BackendApplicationContribution>,
|
||||
@inject(BackendApplicationCliContribution)
|
||||
protected readonly cliParams: BackendApplicationCliContribution
|
||||
) {
|
||||
super(contributionsProvider, cliParams);
|
||||
// Workaround for Electron not installing a handler to ignore SIGPIPE
|
||||
// (https://github.com/electron/electron/issues/13254)
|
||||
// From VS Code: https://github.com/microsoft/vscode/blob/d0c90c9f3ea8d34912194176241503a44b3abd80/src/bootstrap.js#L31-L37
|
||||
process.on('SIGPIPE', () => console.error(new Error('Unexpected SIGPIPE signal.')));
|
||||
process.on('SIGPIPE', () =>
|
||||
console.error(new Error('Unexpected SIGPIPE signal.'))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,12 @@ import { EnvVariablesServerImpl as TheiaEnvVariablesServerImpl } from '@theia/co
|
||||
|
||||
@injectable()
|
||||
export class EnvVariablesServer extends TheiaEnvVariablesServerImpl {
|
||||
|
||||
protected readonly configDirUri = Promise.resolve(FileUri.create(join(homedir(), BackendApplicationConfigProvider.get().configDirName)).toString());
|
||||
|
||||
protected readonly configDirUri = Promise.resolve(
|
||||
FileUri.create(
|
||||
join(
|
||||
homedir(),
|
||||
BackendApplicationConfigProvider.get().configDirName
|
||||
)
|
||||
).toString()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
|
||||
@injectable()
|
||||
export class DefaultGitInit implements GitInit {
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
async init(): Promise<void> {
|
||||
@@ -16,19 +15,35 @@ export class DefaultGitInit implements GitInit {
|
||||
const { execPath, path, version } = await findGit();
|
||||
if (!!execPath && !!path && !!version) {
|
||||
const dir = dirname(dirname(path));
|
||||
const [execPathOk, pathOk, dirOk] = await Promise.all([pathExists(execPath), pathExists(path), pathExists(dir)]);
|
||||
const [execPathOk, pathOk, dirOk] = await Promise.all([
|
||||
pathExists(execPath),
|
||||
pathExists(path),
|
||||
pathExists(dir),
|
||||
]);
|
||||
if (execPathOk && pathOk && dirOk) {
|
||||
if (typeof env.LOCAL_GIT_DIRECTORY !== 'undefined' && env.LOCAL_GIT_DIRECTORY !== dir) {
|
||||
console.error(`Misconfigured env.LOCAL_GIT_DIRECTORY: ${env.LOCAL_GIT_DIRECTORY}. dir was: ${dir}`);
|
||||
if (
|
||||
typeof env.LOCAL_GIT_DIRECTORY !== 'undefined' &&
|
||||
env.LOCAL_GIT_DIRECTORY !== dir
|
||||
) {
|
||||
console.error(
|
||||
`Misconfigured env.LOCAL_GIT_DIRECTORY: ${env.LOCAL_GIT_DIRECTORY}. dir was: ${dir}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (typeof env.GIT_EXEC_PATH !== 'undefined' && env.GIT_EXEC_PATH !== execPath) {
|
||||
console.error(`Misconfigured env.GIT_EXEC_PATH: ${env.GIT_EXEC_PATH}. execPath was: ${execPath}`);
|
||||
if (
|
||||
typeof env.GIT_EXEC_PATH !== 'undefined' &&
|
||||
env.GIT_EXEC_PATH !== execPath
|
||||
) {
|
||||
console.error(
|
||||
`Misconfigured env.GIT_EXEC_PATH: ${env.GIT_EXEC_PATH}. execPath was: ${execPath}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
process.env.LOCAL_GIT_DIRECTORY = dir;
|
||||
process.env.GIT_EXEC_PATH = execPath;
|
||||
console.info(`Using Git [${version}] from the PATH. (${path})`);
|
||||
console.info(
|
||||
`Using Git [${version}] from the PATH. (${path})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -40,5 +55,4 @@ export class DefaultGitInit implements GitInit {
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ConfigService } from '../../../common/protocol/config-service';
|
||||
|
||||
@injectable()
|
||||
export class DefaultWorkspaceServer extends TheiaDefaultWorkspaceServer {
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@@ -17,9 +16,10 @@ export class DefaultWorkspaceServer extends TheiaDefaultWorkspaceServer {
|
||||
const config = await this.configService.getConfiguration();
|
||||
return config.sketchDirUri;
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to determine the sketch directory: ${err}`);
|
||||
this.logger.error(
|
||||
`Failed to determine the sketch directory: ${err}`
|
||||
);
|
||||
return super.getWorkspaceURIFromCli();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user