mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-06-07 04:36:33 +00:00
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:
parent
767b09d2f1
commit
49d12d99ff
@ -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"
|
||||||
|
@ -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');
|
||||||
|
@ -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',
|
||||||
|
@ -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();
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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.
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user