IDE to run CLI with auto assigned port (#673)

* get daemon port from CLI stdout

* config-service to use CLI daemon port

* updating LS

* fixed tests

* fix upload blocked when selectedBoard.port is undefined

* bump arduino-cli to 0.20.2

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
This commit is contained in:
Francesco Stasi 2021-12-09 15:08:26 +01:00 committed by GitHub
parent 767b09d2f1
commit 49d12d99ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 88 additions and 75 deletions

View File

@ -151,7 +151,7 @@
], ],
"arduino": { "arduino": {
"cli": { "cli": {
"version": "0.20.1" "version": "0.20.2"
}, },
"fwuploader": { "fwuploader": {
"version": "2.0.0" "version": "2.0.0"

View File

@ -4,7 +4,7 @@
// - https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${VERSION}_${SUFFIX} // - https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${VERSION}_${SUFFIX}
(() => { (() => {
const DEFAULT_ALS_VERSION = '0.5.0-rc2'; const DEFAULT_ALS_VERSION = '0.5.0-rc6';
const DEFAULT_CLANGD_VERSION = 'snapshot_20210124'; const DEFAULT_CLANGD_VERSION = 'snapshot_20210124';
const path = require('path'); const path = require('path');

View File

@ -374,7 +374,7 @@ export class ArduinoFrontendContribution
'arduino.languageserver.start', 'arduino.languageserver.start',
{ {
lsPath, lsPath,
cliDaemonAddr: `localhost:${config.daemon.port}`, cliDaemonAddr: `localhost:${config.daemon.port}`, // TODO: verify if this port is coming from the BE
clangdPath, clangdPath,
log: currentSketchPath ? currentSketchPath : log, log: currentSketchPath ? currentSketchPath : log,
cliDaemonInstance: '1', cliDaemonInstance: '1',

View File

@ -63,7 +63,9 @@ export class UploadSketch extends SketchContribution {
if (!fqbn) { if (!fqbn) {
return ''; return '';
} }
const address = boardsConfig.selectedBoard?.port?.address; const address =
boardsConfig.selectedBoard?.port?.address ||
boardsConfig.selectedPort?.address;
if (!address) { if (!address) {
return ''; return '';
} }
@ -277,8 +279,8 @@ export class UploadSketch extends SketchContribution {
{ timeout: 3000 } { timeout: 3000 }
); );
} catch (e) { } catch (e) {
let errorMessage = ""; let errorMessage = '';
if (typeof e === "string") { if (typeof e === 'string') {
errorMessage = e; errorMessage = e;
} else { } else {
errorMessage = e.toString(); errorMessage = e.toString();

View File

@ -2,4 +2,5 @@ export const ArduinoDaemonPath = '/services/arduino-daemon';
export const ArduinoDaemon = Symbol('ArduinoDaemon'); export const ArduinoDaemon = Symbol('ArduinoDaemon');
export interface ArduinoDaemon { export interface ArduinoDaemon {
isRunning(): Promise<boolean>; isRunning(): Promise<boolean>;
getPort(): Promise<string>;
} }

View File

@ -42,6 +42,7 @@ export class ArduinoDaemonImpl
protected _running = false; protected _running = false;
protected _ready = new Deferred<void>(); protected _ready = new Deferred<void>();
protected _execPath: string | undefined; protected _execPath: string | undefined;
protected _port: string;
// Backend application lifecycle. // Backend application lifecycle.
@ -55,12 +56,17 @@ export class ArduinoDaemonImpl
return Promise.resolve(this._running); return Promise.resolve(this._running);
} }
async getPort(): Promise<string> {
return Promise.resolve(this._port);
}
async startDaemon(): Promise<void> { async startDaemon(): Promise<void> {
try { try {
this.toDispose.dispose(); // This will `kill` the previously started daemon process, if any. this.toDispose.dispose(); // This will `kill` the previously started daemon process, if any.
const cliPath = await this.getExecPath(); const cliPath = await this.getExecPath();
this.onData(`Starting daemon from ${cliPath}...`); this.onData(`Starting daemon from ${cliPath}...`);
const daemon = await this.spawnDaemonProcess(); const { daemon, port } = await this.spawnDaemonProcess();
this._port = port;
// Watchdog process for terminating the daemon process when the backend app terminates. // Watchdog process for terminating the daemon process when the backend app terminates.
spawn( spawn(
process.execPath, process.execPath,
@ -148,6 +154,10 @@ export class ArduinoDaemonImpl
const cliConfigPath = join(FileUri.fsPath(configDirUri), CLI_CONFIG); const cliConfigPath = join(FileUri.fsPath(configDirUri), CLI_CONFIG);
return [ return [
'daemon', 'daemon',
'--format',
'jsonmini',
'--port',
'0',
'--config-file', '--config-file',
`"${cliConfigPath}"`, `"${cliConfigPath}"`,
'-v', '-v',
@ -156,12 +166,15 @@ export class ArduinoDaemonImpl
]; ];
} }
protected async spawnDaemonProcess(): Promise<ChildProcess> { protected async spawnDaemonProcess(): Promise<{
daemon: ChildProcess;
port: string;
}> {
const [cliPath, args] = await Promise.all([ const [cliPath, args] = await Promise.all([
this.getExecPath(), this.getExecPath(),
this.getSpawnArgs(), this.getSpawnArgs(),
]); ]);
const ready = new Deferred<ChildProcess>(); const ready = new Deferred<{ daemon: ChildProcess; port: string }>();
const options = { shell: true }; const options = { shell: true };
const daemon = spawn(`"${cliPath}"`, args, options); const daemon = spawn(`"${cliPath}"`, args, options);
@ -171,20 +184,37 @@ export class ArduinoDaemonImpl
daemon.stdout.on('data', (data) => { daemon.stdout.on('data', (data) => {
const message = data.toString(); const message = data.toString();
let port = '';
let address = '';
message
.split('\n')
.filter((line: string) => line.length)
.forEach((line: string) => {
try {
const parsedLine = JSON.parse(line);
if ('Port' in parsedLine) {
port = parsedLine.Port;
}
if ('IP' in parsedLine) {
address = parsedLine.IP;
}
} catch (err) {
// ignore
}
});
this.onData(message); this.onData(message);
if (!grpcServerIsReady) { if (!grpcServerIsReady) {
const error = DaemonError.parse(message); const error = DaemonError.parse(message);
if (error) { if (error) {
ready.reject(error); ready.reject(error);
return;
} }
for (const expected of [
'Daemon is listening on TCP port', if (port.length && address.length) {
'Daemon is now listening on 127.0.0.1', grpcServerIsReady = true;
]) { ready.resolve({ daemon, port });
if (message.includes(expected)) {
grpcServerIsReady = true;
ready.resolve(daemon);
}
} }
} }
}); });

View File

@ -75,9 +75,11 @@ export class ConfigServiceImpl
async getConfiguration(): Promise<Config> { async getConfiguration(): Promise<Config> {
await this.ready.promise; await this.ready.promise;
return this.config; await this.daemon.ready;
return { ...this.config, daemon: { port: await this.daemon.getPort() } };
} }
// Used by frontend to update the config.
async setConfiguration(config: Config): Promise<void> { async setConfiguration(config: Config): Promise<void> {
await this.ready.promise; await this.ready.promise;
if (Config.sameAs(this.config, config)) { if (Config.sameAs(this.config, config)) {
@ -108,7 +110,9 @@ export class ConfigServiceImpl
copyDefaultCliConfig.locale = locale || 'en'; copyDefaultCliConfig.locale = locale || 'en';
const proxy = Network.stringify(network); const proxy = Network.stringify(network);
copyDefaultCliConfig.network = { proxy }; copyDefaultCliConfig.network = { proxy };
const { port } = copyDefaultCliConfig.daemon;
// always use the port of the daemon
const port = await this.daemon.getPort();
await this.updateDaemon(port, copyDefaultCliConfig); await this.updateDaemon(port, copyDefaultCliConfig);
await this.writeDaemonState(port); await this.writeDaemonState(port);

View File

@ -48,9 +48,9 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
this._initialized = new Deferred<void>(); this._initialized = new Deferred<void>();
} }
protected async reconcileClient( protected async reconcileClient(): Promise<void> {
port: string | number | undefined const port = await this.daemon.getPort();
): Promise<void> {
if (port && port === this._port) { if (port && port === this._port) {
// No need to create a new gRPC client, but we have to update the indexes. // No need to create a new gRPC client, but we have to update the indexes.
if (this._client && !(this._client instanceof Error)) { if (this._client && !(this._client instanceof Error)) {
@ -58,7 +58,7 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
this.onClientReadyEmitter.fire(); this.onClientReadyEmitter.fire();
} }
} else { } else {
await super.reconcileClient(port); await super.reconcileClient();
this.onClientReadyEmitter.fire(); this.onClientReadyEmitter.fire();
} }
} }
@ -66,13 +66,10 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
@postConstruct() @postConstruct()
protected async init(): Promise<void> { protected async init(): Promise<void> {
this.daemon.ready.then(async () => { this.daemon.ready.then(async () => {
const cliConfig = this.configService.cliConfiguration;
// First create the client and the instance synchronously // First create the client and the instance synchronously
// and notify client is ready. // and notify client is ready.
// TODO: Creation failure should probably be handled here // TODO: Creation failure should probably be handled here
await this.reconcileClient( await this.reconcileClient().then(() => {
cliConfig ? cliConfig.daemon.port : undefined
).then(() => {
this._created.resolve(); this._created.resolve();
}); });

View File

@ -21,8 +21,7 @@ export abstract class GrpcClientProvider<C> {
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
const updateClient = () => { const updateClient = () => {
const cliConfig = this.configService.cliConfiguration; this.reconcileClient();
this.reconcileClient(cliConfig ? cliConfig.daemon.port : undefined);
}; };
this.configService.onConfigChange(updateClient); this.configService.onConfigChange(updateClient);
this.daemon.ready.then(updateClient); this.daemon.ready.then(updateClient);
@ -44,9 +43,9 @@ export abstract class GrpcClientProvider<C> {
} }
} }
protected async reconcileClient( protected async reconcileClient(): Promise<void> {
port: string | number | undefined const port = await this.daemon.getPort();
): Promise<void> {
if (this._port === port) { if (this._port === port) {
return; // Nothing to do. return; // Nothing to do.
} }

View File

@ -2,21 +2,17 @@ import * as fs from 'fs';
// import * as net from 'net'; // import * as net from 'net';
import * as path from 'path'; import * as path from 'path';
import * as temp from 'temp'; import * as temp from 'temp';
import { fail } from 'assert';
import { expect } from 'chai'; import { expect } from 'chai';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import { safeLoad, safeDump } from 'js-yaml'; import { safeLoad, safeDump } from 'js-yaml';
import { DaemonError, ArduinoDaemonImpl } from '../../node/arduino-daemon-impl'; import { ArduinoDaemonImpl } from '../../node/arduino-daemon-impl';
import { spawnCommand } from '../../node/exec-util'; import { spawnCommand } from '../../node/exec-util';
import { CLI_CONFIG } from '../../node/cli-config'; import { CLI_CONFIG } from '../../node/cli-config';
const track = temp.track(); const track = temp.track();
class SilentArduinoDaemonImpl extends ArduinoDaemonImpl { class SilentArduinoDaemonImpl extends ArduinoDaemonImpl {
constructor( constructor(private logFormat: 'text' | 'json') {
private port: string | number,
private logFormat: 'text' | 'json'
) {
super(); super();
} }
@ -24,7 +20,7 @@ class SilentArduinoDaemonImpl extends ArduinoDaemonImpl {
// NOOP // NOOP
} }
async spawnDaemonProcess(): Promise<ChildProcess> { async spawnDaemonProcess(): Promise<{ daemon: ChildProcess; port: string }> {
return super.spawnDaemonProcess(); return super.spawnDaemonProcess();
} }
@ -32,6 +28,10 @@ class SilentArduinoDaemonImpl extends ArduinoDaemonImpl {
const cliConfigPath = await this.initCliConfig(); const cliConfigPath = await this.initCliConfig();
return [ return [
'daemon', 'daemon',
'--format',
'jsonmini',
'--port',
'0',
'--config-file', '--config-file',
cliConfigPath, cliConfigPath,
'-v', '-v',
@ -53,7 +53,7 @@ class SilentArduinoDaemonImpl extends ArduinoDaemonImpl {
encoding: 'utf8', encoding: 'utf8',
}); });
const cliConfig = safeLoad(content) as any; const cliConfig = safeLoad(content) as any;
cliConfig.daemon.port = String(this.port); // cliConfig.daemon.port = String(this.port);
const modifiedContent = safeDump(cliConfig); const modifiedContent = safeDump(cliConfig);
fs.writeFileSync(path.join(destDir, CLI_CONFIG), modifiedContent, { fs.writeFileSync(path.join(destDir, CLI_CONFIG), modifiedContent, {
encoding: 'utf8', encoding: 'utf8',
@ -113,43 +113,23 @@ describe('arduino-daemon-impl', () => {
// } // }
// }); // });
it('should parse an error - unknown address [json]', async () => { it('should parse the port address when the log format is json', async () => {
try { const { daemon, port } = await new SilentArduinoDaemonImpl(
await new SilentArduinoDaemonImpl('foo', 'json').spawnDaemonProcess(); 'json'
fail('Expected a failure.'); ).spawnDaemonProcess();
} catch (e) {
expect(e).to.be.instanceOf(DaemonError); expect(port).not.to.be.undefined;
expect(e.code).to.be.equal(DaemonError.UNKNOWN_ADDRESS); expect(port).not.to.be.equal('0');
} daemon.kill();
}); });
it('should parse an error - unknown address [text]', async () => { it('should parse the port address when the log format is text', async () => {
try { const { daemon, port } = await new SilentArduinoDaemonImpl(
await new SilentArduinoDaemonImpl('foo', 'text').spawnDaemonProcess(); 'text'
fail('Expected a failure.'); ).spawnDaemonProcess();
} catch (e) {
expect(e).to.be.instanceOf(DaemonError);
expect(e.code).to.be.equal(DaemonError.UNKNOWN_ADDRESS);
}
});
it('should parse an error - invalid port [json]', async () => { expect(port).not.to.be.undefined;
try { expect(port).not.to.be.equal('0');
await new SilentArduinoDaemonImpl(-1, 'json').spawnDaemonProcess(); daemon.kill();
fail('Expected a failure.');
} catch (e) {
expect(e).to.be.instanceOf(DaemonError);
expect(e.code).to.be.equal(DaemonError.INVALID_PORT);
}
});
it('should parse an error - invalid port [text]', async () => {
try {
await new SilentArduinoDaemonImpl(-1, 'text').spawnDaemonProcess();
fail('Expected a failure.');
} catch (e) {
expect(e).to.be.instanceOf(DaemonError);
expect(e.code).to.be.equal(DaemonError.INVALID_PORT);
}
}); });
}); });