Skip to content

Commit 49d12d9

Browse files
fstasiAlberto Iannaccone
and
Alberto Iannaccone
authored
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 <[email protected]>
1 parent 767b09d commit 49d12d9

10 files changed

+87
-74
lines changed

Diff for: arduino-ide-extension/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
],
152152
"arduino": {
153153
"cli": {
154-
"version": "0.20.1"
154+
"version": "0.20.2"
155155
},
156156
"fwuploader": {
157157
"version": "2.0.0"

Diff for: arduino-ide-extension/scripts/download-ls.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// - https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${VERSION}_${SUFFIX}
55

66
(() => {
7-
const DEFAULT_ALS_VERSION = '0.5.0-rc2';
7+
const DEFAULT_ALS_VERSION = '0.5.0-rc6';
88
const DEFAULT_CLANGD_VERSION = 'snapshot_20210124';
99

1010
const path = require('path');

Diff for: arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ export class ArduinoFrontendContribution
374374
'arduino.languageserver.start',
375375
{
376376
lsPath,
377-
cliDaemonAddr: `localhost:${config.daemon.port}`,
377+
cliDaemonAddr: `localhost:${config.daemon.port}`, // TODO: verify if this port is coming from the BE
378378
clangdPath,
379379
log: currentSketchPath ? currentSketchPath : log,
380380
cliDaemonInstance: '1',

Diff for: arduino-ide-extension/src/browser/contributions/upload-sketch.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ export class UploadSketch extends SketchContribution {
6363
if (!fqbn) {
6464
return '';
6565
}
66-
const address = boardsConfig.selectedBoard?.port?.address;
66+
const address =
67+
boardsConfig.selectedBoard?.port?.address ||
68+
boardsConfig.selectedPort?.address;
6769
if (!address) {
6870
return '';
6971
}
@@ -277,8 +279,8 @@ export class UploadSketch extends SketchContribution {
277279
{ timeout: 3000 }
278280
);
279281
} catch (e) {
280-
let errorMessage = "";
281-
if (typeof e === "string") {
282+
let errorMessage = '';
283+
if (typeof e === 'string') {
282284
errorMessage = e;
283285
} else {
284286
errorMessage = e.toString();

Diff for: arduino-ide-extension/src/common/protocol/arduino-daemon.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export const ArduinoDaemonPath = '/services/arduino-daemon';
22
export const ArduinoDaemon = Symbol('ArduinoDaemon');
33
export interface ArduinoDaemon {
44
isRunning(): Promise<boolean>;
5+
getPort(): Promise<string>;
56
}

Diff for: arduino-ide-extension/src/node/arduino-daemon-impl.ts

+41-11
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export class ArduinoDaemonImpl
4242
protected _running = false;
4343
protected _ready = new Deferred<void>();
4444
protected _execPath: string | undefined;
45+
protected _port: string;
4546

4647
// Backend application lifecycle.
4748

@@ -55,12 +56,17 @@ export class ArduinoDaemonImpl
5556
return Promise.resolve(this._running);
5657
}
5758

59+
async getPort(): Promise<string> {
60+
return Promise.resolve(this._port);
61+
}
62+
5863
async startDaemon(): Promise<void> {
5964
try {
6065
this.toDispose.dispose(); // This will `kill` the previously started daemon process, if any.
6166
const cliPath = await this.getExecPath();
6267
this.onData(`Starting daemon from ${cliPath}...`);
63-
const daemon = await this.spawnDaemonProcess();
68+
const { daemon, port } = await this.spawnDaemonProcess();
69+
this._port = port;
6470
// Watchdog process for terminating the daemon process when the backend app terminates.
6571
spawn(
6672
process.execPath,
@@ -148,6 +154,10 @@ export class ArduinoDaemonImpl
148154
const cliConfigPath = join(FileUri.fsPath(configDirUri), CLI_CONFIG);
149155
return [
150156
'daemon',
157+
'--format',
158+
'jsonmini',
159+
'--port',
160+
'0',
151161
'--config-file',
152162
`"${cliConfigPath}"`,
153163
'-v',
@@ -156,12 +166,15 @@ export class ArduinoDaemonImpl
156166
];
157167
}
158168

159-
protected async spawnDaemonProcess(): Promise<ChildProcess> {
169+
protected async spawnDaemonProcess(): Promise<{
170+
daemon: ChildProcess;
171+
port: string;
172+
}> {
160173
const [cliPath, args] = await Promise.all([
161174
this.getExecPath(),
162175
this.getSpawnArgs(),
163176
]);
164-
const ready = new Deferred<ChildProcess>();
177+
const ready = new Deferred<{ daemon: ChildProcess; port: string }>();
165178
const options = { shell: true };
166179
const daemon = spawn(`"${cliPath}"`, args, options);
167180

@@ -171,20 +184,37 @@ export class ArduinoDaemonImpl
171184

172185
daemon.stdout.on('data', (data) => {
173186
const message = data.toString();
187+
188+
let port = '';
189+
let address = '';
190+
message
191+
.split('\n')
192+
.filter((line: string) => line.length)
193+
.forEach((line: string) => {
194+
try {
195+
const parsedLine = JSON.parse(line);
196+
if ('Port' in parsedLine) {
197+
port = parsedLine.Port;
198+
}
199+
if ('IP' in parsedLine) {
200+
address = parsedLine.IP;
201+
}
202+
} catch (err) {
203+
// ignore
204+
}
205+
});
206+
174207
this.onData(message);
175208
if (!grpcServerIsReady) {
176209
const error = DaemonError.parse(message);
177210
if (error) {
178211
ready.reject(error);
212+
return;
179213
}
180-
for (const expected of [
181-
'Daemon is listening on TCP port',
182-
'Daemon is now listening on 127.0.0.1',
183-
]) {
184-
if (message.includes(expected)) {
185-
grpcServerIsReady = true;
186-
ready.resolve(daemon);
187-
}
214+
215+
if (port.length && address.length) {
216+
grpcServerIsReady = true;
217+
ready.resolve({ daemon, port });
188218
}
189219
}
190220
});

Diff for: arduino-ide-extension/src/node/config-service-impl.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ export class ConfigServiceImpl
7575

7676
async getConfiguration(): Promise<Config> {
7777
await this.ready.promise;
78-
return this.config;
78+
await this.daemon.ready;
79+
return { ...this.config, daemon: { port: await this.daemon.getPort() } };
7980
}
8081

82+
// Used by frontend to update the config.
8183
async setConfiguration(config: Config): Promise<void> {
8284
await this.ready.promise;
8385
if (Config.sameAs(this.config, config)) {
@@ -108,7 +110,9 @@ export class ConfigServiceImpl
108110
copyDefaultCliConfig.locale = locale || 'en';
109111
const proxy = Network.stringify(network);
110112
copyDefaultCliConfig.network = { proxy };
111-
const { port } = copyDefaultCliConfig.daemon;
113+
114+
// always use the port of the daemon
115+
const port = await this.daemon.getPort();
112116
await this.updateDaemon(port, copyDefaultCliConfig);
113117
await this.writeDaemonState(port);
114118

Diff for: arduino-ide-extension/src/node/core-client-provider.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -48,31 +48,28 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
4848
this._initialized = new Deferred<void>();
4949
}
5050

51-
protected async reconcileClient(
52-
port: string | number | undefined
53-
): Promise<void> {
51+
protected async reconcileClient(): Promise<void> {
52+
const port = await this.daemon.getPort();
53+
5454
if (port && port === this._port) {
5555
// No need to create a new gRPC client, but we have to update the indexes.
5656
if (this._client && !(this._client instanceof Error)) {
5757
await this.updateIndexes(this._client);
5858
this.onClientReadyEmitter.fire();
5959
}
6060
} else {
61-
await super.reconcileClient(port);
61+
await super.reconcileClient();
6262
this.onClientReadyEmitter.fire();
6363
}
6464
}
6565

6666
@postConstruct()
6767
protected async init(): Promise<void> {
6868
this.daemon.ready.then(async () => {
69-
const cliConfig = this.configService.cliConfiguration;
7069
// First create the client and the instance synchronously
7170
// and notify client is ready.
7271
// TODO: Creation failure should probably be handled here
73-
await this.reconcileClient(
74-
cliConfig ? cliConfig.daemon.port : undefined
75-
).then(() => {
72+
await this.reconcileClient().then(() => {
7673
this._created.resolve();
7774
});
7875

Diff for: arduino-ide-extension/src/node/grpc-client-provider.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ export abstract class GrpcClientProvider<C> {
2121
@postConstruct()
2222
protected init(): void {
2323
const updateClient = () => {
24-
const cliConfig = this.configService.cliConfiguration;
25-
this.reconcileClient(cliConfig ? cliConfig.daemon.port : undefined);
24+
this.reconcileClient();
2625
};
2726
this.configService.onConfigChange(updateClient);
2827
this.daemon.ready.then(updateClient);
@@ -44,9 +43,9 @@ export abstract class GrpcClientProvider<C> {
4443
}
4544
}
4645

47-
protected async reconcileClient(
48-
port: string | number | undefined
49-
): Promise<void> {
46+
protected async reconcileClient(): Promise<void> {
47+
const port = await this.daemon.getPort();
48+
5049
if (this._port === port) {
5150
return; // Nothing to do.
5251
}

Diff for: arduino-ide-extension/src/test/node/arduino-daemon-impl.test.ts

+22-42
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,36 @@ import * as fs from 'fs';
22
// import * as net from 'net';
33
import * as path from 'path';
44
import * as temp from 'temp';
5-
import { fail } from 'assert';
65
import { expect } from 'chai';
76
import { ChildProcess } from 'child_process';
87
import { safeLoad, safeDump } from 'js-yaml';
9-
import { DaemonError, ArduinoDaemonImpl } from '../../node/arduino-daemon-impl';
8+
import { ArduinoDaemonImpl } from '../../node/arduino-daemon-impl';
109
import { spawnCommand } from '../../node/exec-util';
1110
import { CLI_CONFIG } from '../../node/cli-config';
1211

1312
const track = temp.track();
1413

1514
class SilentArduinoDaemonImpl extends ArduinoDaemonImpl {
16-
constructor(
17-
private port: string | number,
18-
private logFormat: 'text' | 'json'
19-
) {
15+
constructor(private logFormat: 'text' | 'json') {
2016
super();
2117
}
2218

2319
onData(data: string): void {
2420
// NOOP
2521
}
2622

27-
async spawnDaemonProcess(): Promise<ChildProcess> {
23+
async spawnDaemonProcess(): Promise<{ daemon: ChildProcess; port: string }> {
2824
return super.spawnDaemonProcess();
2925
}
3026

3127
protected async getSpawnArgs(): Promise<string[]> {
3228
const cliConfigPath = await this.initCliConfig();
3329
return [
3430
'daemon',
31+
'--format',
32+
'jsonmini',
33+
'--port',
34+
'0',
3535
'--config-file',
3636
cliConfigPath,
3737
'-v',
@@ -53,7 +53,7 @@ class SilentArduinoDaemonImpl extends ArduinoDaemonImpl {
5353
encoding: 'utf8',
5454
});
5555
const cliConfig = safeLoad(content) as any;
56-
cliConfig.daemon.port = String(this.port);
56+
// cliConfig.daemon.port = String(this.port);
5757
const modifiedContent = safeDump(cliConfig);
5858
fs.writeFileSync(path.join(destDir, CLI_CONFIG), modifiedContent, {
5959
encoding: 'utf8',
@@ -113,43 +113,23 @@ describe('arduino-daemon-impl', () => {
113113
// }
114114
// });
115115

116-
it('should parse an error - unknown address [json]', async () => {
117-
try {
118-
await new SilentArduinoDaemonImpl('foo', 'json').spawnDaemonProcess();
119-
fail('Expected a failure.');
120-
} catch (e) {
121-
expect(e).to.be.instanceOf(DaemonError);
122-
expect(e.code).to.be.equal(DaemonError.UNKNOWN_ADDRESS);
123-
}
124-
});
116+
it('should parse the port address when the log format is json', async () => {
117+
const { daemon, port } = await new SilentArduinoDaemonImpl(
118+
'json'
119+
).spawnDaemonProcess();
125120

126-
it('should parse an error - unknown address [text]', async () => {
127-
try {
128-
await new SilentArduinoDaemonImpl('foo', 'text').spawnDaemonProcess();
129-
fail('Expected a failure.');
130-
} catch (e) {
131-
expect(e).to.be.instanceOf(DaemonError);
132-
expect(e.code).to.be.equal(DaemonError.UNKNOWN_ADDRESS);
133-
}
121+
expect(port).not.to.be.undefined;
122+
expect(port).not.to.be.equal('0');
123+
daemon.kill();
134124
});
135125

136-
it('should parse an error - invalid port [json]', async () => {
137-
try {
138-
await new SilentArduinoDaemonImpl(-1, 'json').spawnDaemonProcess();
139-
fail('Expected a failure.');
140-
} catch (e) {
141-
expect(e).to.be.instanceOf(DaemonError);
142-
expect(e.code).to.be.equal(DaemonError.INVALID_PORT);
143-
}
144-
});
126+
it('should parse the port address when the log format is text', async () => {
127+
const { daemon, port } = await new SilentArduinoDaemonImpl(
128+
'text'
129+
).spawnDaemonProcess();
145130

146-
it('should parse an error - invalid port [text]', async () => {
147-
try {
148-
await new SilentArduinoDaemonImpl(-1, 'text').spawnDaemonProcess();
149-
fail('Expected a failure.');
150-
} catch (e) {
151-
expect(e).to.be.instanceOf(DaemonError);
152-
expect(e.code).to.be.equal(DaemonError.INVALID_PORT);
153-
}
131+
expect(port).not.to.be.undefined;
132+
expect(port).not.to.be.equal('0');
133+
daemon.kill();
154134
});
155135
});

0 commit comments

Comments
 (0)