Skip to content

Commit 3a56c16

Browse files
authored
Merge pull request #95 from bcmi-labs/pro-ide-142
Align language server spawning with arduino-cli
2 parents 2577451 + cea62e3 commit 3a56c16

File tree

10 files changed

+587
-134
lines changed

10 files changed

+587
-134
lines changed

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ node_modules/
22
# .node_modules is a hack for the electron builder.
33
.node_modules/
44
lib/
5-
build/
65
downloads/
6+
build/
77
!electron/build/
88
src-gen/
99
browser-app/webpack.config.js
1010
electron-app/webpack.config.js
11-
.DS_Store
1211
/workspace/static
12+
.DS_Store
1313
# switching from `electron` to `browser` in dev mode.
1414
.browser_modules
1515
# LS logs
1616
inols*.log
17+
yarn-error.log

arduino-ide-extension/package.json

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
"@theia/search-in-workspace": "next",
2323
"@theia/terminal": "next",
2424
"@theia/workspace": "next",
25-
"@types/google-protobuf": "^3.7.1",
2625
"@types/dateformat": "^3.0.1",
26+
"@types/google-protobuf": "^3.7.1",
2727
"@types/ps-tree": "^1.1.0",
2828
"@types/react-select": "^3.0.0",
2929
"@types/which": "^1.3.1",
@@ -47,19 +47,37 @@
4747
"generate-protocol": "node ./scripts/generate-protocol.js",
4848
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
4949
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
50-
"watch": "tsc -w"
50+
"watch": "tsc -w",
51+
"test": "mocha \"./test/**/*.test.ts\""
52+
},
53+
"mocha": {
54+
"require": [
55+
"ts-node/register",
56+
"reflect-metadata/Reflect"
57+
],
58+
"reporter": "spec",
59+
"colors": true,
60+
"watch-extensions": "ts,tsx",
61+
"timeout": 10000
5162
},
5263
"devDependencies": {
64+
"@types/chai": "^4.2.7",
65+
"@types/chai-string": "^1.4.2",
66+
"@types/mocha": "^5.2.7",
67+
"chai": "^4.2.0",
68+
"chai-string": "^1.5.0",
5369
"decompress": "^4.2.0",
5470
"decompress-targz": "^4.1.1",
5571
"decompress-unzip": "^4.0.1",
5672
"download": "^7.1.0",
5773
"grpc-tools": "^1.8.0",
5874
"grpc_tools_node_protoc_ts": "^2.5.8",
75+
"mocha": "^7.0.0",
5976
"moment": "^2.24.0",
6077
"ncp": "^2.0.0",
6178
"rimraf": "^2.6.1",
6279
"shelljs": "^0.8.3",
80+
"ts-node": "^8.6.2",
6381
"tslint": "^5.5.0",
6482
"typescript": "3.5.3",
6583
"uuid": "^3.2.1",
Lines changed: 6 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
import * as os from 'os';
2-
import * as which from 'which';
3-
import * as semver from 'semver';
4-
import { spawn } from 'child_process';
5-
import { join } from 'path';
61
import { injectable, inject } from 'inversify';
72
import { ILogger } from '@theia/core';
83
import { FileUri } from '@theia/core/lib/node/file-uri';
94
import { Config } from '../common/protocol/config-service';
5+
import { spawnCommand, getExecPath } from './exec-util';
106

117
@injectable()
128
export class ArduinoCli {
@@ -20,33 +16,19 @@ export class ArduinoCli {
2016
if (this.execPath) {
2117
return this.execPath;
2218
}
23-
const version = /\d+\.\d+\.\d+/;
24-
const cli = `arduino-cli${os.platform() === 'win32' ? '.exe' : ''}`;
25-
const buildCli = join(__dirname, '..', '..', 'build', cli);
26-
const buildVersion = await this.spawn(`"${buildCli}"`, ['version']);
27-
const buildShortVersion = (buildVersion.match(version) || [])[0];
28-
this.execPath = buildCli;
29-
const pathCli = await new Promise<string | undefined>(resolve => which(cli, (error, path) => resolve(error ? undefined : path)));
30-
if (!pathCli) {
31-
return buildCli;
32-
}
33-
const pathVersion = await this.spawn(`"${pathCli}"`, ['version']);
34-
const pathShortVersion = (pathVersion.match(version) || [])[0];
35-
if (semver.gt(pathShortVersion, buildShortVersion)) {
36-
this.execPath = pathCli;
37-
return pathCli;
38-
}
39-
return buildCli;
19+
const path = await getExecPath('arduino-cli', this.logger, 'version');
20+
this.execPath = path;
21+
return path;
4022
}
4123

4224
async getVersion(): Promise<string> {
4325
const execPath = await this.getExecPath();
44-
return this.spawn(`"${execPath}"`, ['version']);
26+
return spawnCommand(`"${execPath}"`, ['version'], this.logger);
4527
}
4628

4729
async getDefaultConfig(): Promise<Config> {
4830
const execPath = await this.getExecPath();
49-
const result = await this.spawn(`"${execPath}"`, ['config', 'dump', '--format', 'json']);
31+
const result = await spawnCommand(`"${execPath}"`, ['config', 'dump', '--format', 'json'], this.logger);
5032
const { directories } = JSON.parse(result);
5133
if (!directories) {
5234
throw new Error(`Could not parse config. 'directories' was missing from: ${result}`);
@@ -64,33 +46,4 @@ export class ArduinoCli {
6446
};
6547
}
6648

67-
private spawn(command: string, args?: string[]): Promise<string> {
68-
return new Promise<string>((resolve, reject) => {
69-
const buffers: Buffer[] = [];
70-
const cp = spawn(command, args, { windowsHide: true, shell: true });
71-
cp.stdout.on('data', (b: Buffer) => buffers.push(b));
72-
cp.on('error', error => {
73-
this.logger.error(`Error executing ${command} with args: ${JSON.stringify(args)}.`, error);
74-
reject(error);
75-
});
76-
cp.on('exit', (code, signal) => {
77-
if (code === 0) {
78-
const result = Buffer.concat(buffers).toString('utf8').trim()
79-
resolve(result);
80-
return;
81-
}
82-
if (signal) {
83-
this.logger.error(`Unexpected signal '${signal}' when executing ${command} with args: ${JSON.stringify(args)}.`);
84-
reject(new Error(`Process exited with signal: ${signal}`));
85-
return;
86-
}
87-
if (code) {
88-
this.logger.error(`Unexpected exit code '${code}' when executing ${command} with args: ${JSON.stringify(args)}.`);
89-
reject(new Error(`Process exited with exit code: ${code}`));
90-
return;
91-
}
92-
});
93-
});
94-
}
95-
9649
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as os from 'os';
2+
import * as which from 'which';
3+
import * as semver from 'semver';
4+
import { spawn } from 'child_process';
5+
import { join } from 'path';
6+
import { ILogger } from '@theia/core';
7+
8+
export async function getExecPath(commandName: string, logger: ILogger, versionArg?: string, inBinDir?: boolean): Promise<string> {
9+
const execName = `${commandName}${os.platform() === 'win32' ? '.exe' : ''}`;
10+
const relativePath = ['..', '..', 'build'];
11+
if (inBinDir) {
12+
relativePath.push('bin');
13+
}
14+
const buildCommand = join(__dirname, ...relativePath, execName);
15+
if (!versionArg) {
16+
return buildCommand;
17+
}
18+
const versionRegexp = /\d+\.\d+\.\d+/;
19+
const buildVersion = await spawnCommand(`"${buildCommand}"`, [versionArg], logger);
20+
const buildShortVersion = (buildVersion.match(versionRegexp) || [])[0];
21+
const pathCommand = await new Promise<string | undefined>(resolve => which(execName, (error, path) => resolve(error ? undefined : path)));
22+
if (!pathCommand) {
23+
return buildCommand;
24+
}
25+
const pathVersion = await spawnCommand(`"${pathCommand}"`, [versionArg], logger);
26+
const pathShortVersion = (pathVersion.match(versionRegexp) || [])[0];
27+
if (semver.gt(pathShortVersion, buildShortVersion)) {
28+
return pathCommand;
29+
}
30+
return buildCommand;
31+
}
32+
33+
export function spawnCommand(command: string, args: string[], logger: ILogger): Promise<string> {
34+
return new Promise<string>((resolve, reject) => {
35+
const cp = spawn(command, args, { windowsHide: true, shell: true });
36+
const buffers: Buffer[] = [];
37+
cp.stdout.on('data', (b: Buffer) => buffers.push(b));
38+
cp.on('error', error => {
39+
logger.error(`Error executing ${command} with args: ${JSON.stringify(args)}.`, error);
40+
reject(error);
41+
});
42+
cp.on('exit', (code, signal) => {
43+
if (code === 0) {
44+
const result = Buffer.concat(buffers).toString('utf8').trim()
45+
resolve(result);
46+
return;
47+
}
48+
if (signal) {
49+
logger.error(`Unexpected signal '${signal}' when executing ${command} with args: ${JSON.stringify(args)}.`);
50+
reject(new Error(`Process exited with signal: ${signal}`));
51+
return;
52+
}
53+
if (code) {
54+
logger.error(`Unexpected exit code '${code}' when executing ${command} with args: ${JSON.stringify(args)}.`);
55+
reject(new Error(`Process exited with exit code: ${code}`));
56+
return;
57+
}
58+
});
59+
});
60+
}
Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import * as which from 'which';
21
import * as os from 'os';
3-
import { join, delimiter } from 'path';
4-
import { injectable } from 'inversify';
2+
import { injectable, inject } from 'inversify';
3+
import { ILogger } from '@theia/core';
54
import { BaseLanguageServerContribution, IConnection, LanguageServerStartOptions } from '@theia/languages/lib/node';
65
import { Board } from '../../common/protocol/boards-service';
6+
import { getExecPath } from '../exec-util';
77

88
@injectable()
99
export class ArduinoLanguageServerContribution extends BaseLanguageServerContribution {
@@ -23,10 +23,13 @@ export class ArduinoLanguageServerContribution extends BaseLanguageServerContrib
2323
return this.description.name;
2424
}
2525

26+
@inject(ILogger)
27+
protected logger: ILogger;
28+
2629
async start(clientConnection: IConnection, options: LanguageServerStartOptions): Promise<void> {
27-
const languageServer = await this.resolveExecutable('arduino-language-server');
28-
const clangd = await this.resolveExecutable('clangd');
29-
const cli = await this.resolveExecutable('arduino-cli');
30+
const languageServer = await getExecPath('arduino-language-server', this.logger);
31+
const clangd = await getExecPath('clangd', this.logger, '--version', os.platform() !== 'win32');
32+
const cli = await getExecPath('arduino-cli', this.logger, 'version');
3033
// Add '-log' argument to enable logging to files
3134
const args: string[] = ['-clangd', clangd, '-cli', cli];
3235
if (options.parameters && options.parameters.selectedBoard) {
@@ -45,21 +48,4 @@ export class ArduinoLanguageServerContribution extends BaseLanguageServerContrib
4548
serverConnection.onClose(() => (clientConnection as any).reader.socket.close());
4649
}
4750

48-
protected resolveExecutable(name: string): Promise<string> {
49-
return new Promise<string>((resolve, reject) => {
50-
const segments = ['..', '..', '..', 'build'];
51-
if (name === 'clangd' && os.platform() !== 'win32') {
52-
segments.push('bin');
53-
}
54-
const path = `${process.env.PATH}${delimiter}${join(__dirname, ...segments)}`;
55-
const suffix = os.platform() === 'win32' ? '.exe' : '';
56-
which(name + suffix, { path }, (err, execPath) => {
57-
if (err) {
58-
reject(err);
59-
} else {
60-
resolve(execPath);
61-
}
62-
});
63-
});
64-
}
6551
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as os from 'os';
2+
import { expect, use } from 'chai';
3+
import { NullLogger } from './logger';
4+
import { getExecPath } from '../../lib/node/exec-util'
5+
6+
use(require('chai-string'));
7+
8+
describe('getExecPath', () => {
9+
it('should resolve arduino-cli', async () => {
10+
const path = await getExecPath('arduino-cli', new NullLogger(), 'version');
11+
if (os.platform() === 'win32')
12+
expect(path).to.endsWith('\\arduino-cli.exe');
13+
else
14+
expect(path).to.endsWith('/arduino-cli');
15+
});
16+
17+
it('should resolve arduino-language-server', async () => {
18+
const path = await getExecPath('arduino-language-server', new NullLogger());
19+
if (os.platform() === 'win32')
20+
expect(path).to.endsWith('\\arduino-language-server.exe');
21+
else
22+
expect(path).to.endsWith('/arduino-language-server');
23+
});
24+
25+
it('should resolve clangd', async () => {
26+
const path = await getExecPath('clangd', new NullLogger(), '--version', os.platform() !== 'win32');
27+
if (os.platform() === 'win32')
28+
expect(path).to.endsWith('\\clangd.exe');
29+
else
30+
expect(path).to.endsWith('/clangd');
31+
});
32+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { ILogger, Loggable, LogLevel } from '@theia/core';
2+
3+
export class NullLogger implements ILogger {
4+
logLevel = 0;
5+
6+
setLogLevel(logLevel: number): Promise<void> {
7+
this.logLevel = logLevel;
8+
return Promise.resolve();
9+
}
10+
getLogLevel(): Promise<number> {
11+
return Promise.resolve(this.logLevel);
12+
}
13+
isEnabled(logLevel: number): Promise<boolean> {
14+
return Promise.resolve(logLevel >= this.logLevel);
15+
}
16+
ifEnabled(logLevel: number): Promise<void> {
17+
if (logLevel >= this.logLevel)
18+
return Promise.resolve();
19+
else
20+
return Promise.reject();
21+
}
22+
log(logLevel: any, loggable: any, ...rest: any[]) {
23+
return Promise.resolve();
24+
}
25+
26+
isTrace(): Promise<boolean> {
27+
return this.isEnabled(LogLevel.TRACE);
28+
}
29+
ifTrace(): Promise<void> {
30+
return this.ifEnabled(LogLevel.TRACE);
31+
}
32+
trace(arg: any | Loggable, ...params: any[]): Promise<void> {
33+
return this.log(LogLevel.TRACE, arg, ...params);
34+
}
35+
36+
isDebug(): Promise<boolean> {
37+
return this.isEnabled(LogLevel.DEBUG);
38+
}
39+
ifDebug(): Promise<void> {
40+
return this.ifEnabled(LogLevel.DEBUG);
41+
}
42+
debug(arg: any | Loggable, ...params: any[]): Promise<void> {
43+
return this.log(LogLevel.DEBUG, arg, ...params);
44+
}
45+
46+
isInfo(): Promise<boolean> {
47+
return this.isEnabled(LogLevel.INFO);
48+
}
49+
ifInfo(): Promise<void> {
50+
return this.ifEnabled(LogLevel.INFO);
51+
}
52+
info(arg: any | Loggable, ...params: any[]): Promise<void> {
53+
return this.log(LogLevel.INFO, arg, ...params);
54+
}
55+
56+
isWarn(): Promise<boolean> {
57+
return this.isEnabled(LogLevel.WARN);
58+
}
59+
ifWarn(): Promise<void> {
60+
return this.ifEnabled(LogLevel.WARN);
61+
}
62+
warn(arg: any | Loggable, ...params: any[]): Promise<void> {
63+
return this.log(LogLevel.WARN, arg, ...params);
64+
}
65+
66+
isError(): Promise<boolean> {
67+
return this.isEnabled(LogLevel.ERROR);
68+
}
69+
ifError(): Promise<void> {
70+
return this.ifEnabled(LogLevel.ERROR);
71+
}
72+
error(arg: any | Loggable, ...params: any[]): Promise<void> {
73+
return this.log(LogLevel.ERROR, arg, ...params);
74+
}
75+
76+
isFatal(): Promise<boolean> {
77+
return this.isEnabled(LogLevel.FATAL);
78+
}
79+
ifFatal(): Promise<void> {
80+
return this.ifEnabled(LogLevel.FATAL);
81+
}
82+
fatal(arg: any | Loggable, ...params: any[]): Promise<void> {
83+
return this.log(LogLevel.FATAL, arg, ...params);
84+
}
85+
86+
child(name: string): ILogger {
87+
return this;
88+
}
89+
}

azure-pipelines.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ jobs:
3434
GITHUB_TOKEN: $(Personal.GitHub.Token)
3535
THEIA_ELECTRON_SKIP_REPLACE_FFMPEG: 1
3636
displayName: Build
37+
- script: yarn test
38+
displayName: Test
3739
- bash: |
3840
./electron/packager/conf-node-gyp.sh
3941
yarn --cwd ./electron/packager/

0 commit comments

Comments
 (0)