mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-14 06:46:36 +00:00
fix: removed unsafe shell
when executing process
Ref: PNX-3671 Co-authored-by: per1234 <accounts@perglass.com> Co-authored-by: Akos Kitta <a.kitta@arduino.cc> Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
e47fb2e651
commit
9d2297c684
@ -104,17 +104,14 @@
|
|||||||
"temp": "^0.9.1",
|
"temp": "^0.9.1",
|
||||||
"temp-dir": "^2.0.0",
|
"temp-dir": "^2.0.0",
|
||||||
"tree-kill": "^1.2.1",
|
"tree-kill": "^1.2.1",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5"
|
||||||
"which": "^1.3.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@octokit/rest": "^18.12.0",
|
"@octokit/rest": "^18.12.0",
|
||||||
"@types/chai": "^4.2.7",
|
"@types/chai": "^4.2.7",
|
||||||
"@types/chai-string": "^1.4.2",
|
|
||||||
"@types/mocha": "^5.2.7",
|
"@types/mocha": "^5.2.7",
|
||||||
"@types/react-window": "^1.8.5",
|
"@types/react-window": "^1.8.5",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"chai-string": "^1.5.0",
|
|
||||||
"decompress": "^4.2.0",
|
"decompress": "^4.2.0",
|
||||||
"decompress-tarbz2": "^4.1.1",
|
"decompress-tarbz2": "^4.1.1",
|
||||||
"decompress-targz": "^4.1.1",
|
"decompress-targz": "^4.1.1",
|
||||||
|
@ -5,6 +5,5 @@ export interface ExecutableService {
|
|||||||
clangdUri: string;
|
clangdUri: string;
|
||||||
cliUri: string;
|
cliUri: string;
|
||||||
lsUri: string;
|
lsUri: string;
|
||||||
fwuploaderUri: string;
|
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,6 @@ export class ArduinoDaemonImpl
|
|||||||
|
|
||||||
private _running = false;
|
private _running = false;
|
||||||
private _port = new Deferred<string>();
|
private _port = new Deferred<string>();
|
||||||
private _execPath: string | undefined;
|
|
||||||
|
|
||||||
// Backend application lifecycle.
|
// Backend application lifecycle.
|
||||||
|
|
||||||
@ -68,7 +67,7 @@ export class ArduinoDaemonImpl
|
|||||||
async start(): Promise<string> {
|
async start(): Promise<string> {
|
||||||
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 = this.getExecPath();
|
||||||
this.onData(`Starting daemon from ${cliPath}...`);
|
this.onData(`Starting daemon from ${cliPath}...`);
|
||||||
const { daemon, port } = await this.spawnDaemonProcess();
|
const { daemon, port } = await this.spawnDaemonProcess();
|
||||||
// Watchdog process for terminating the daemon process when the backend app terminates.
|
// Watchdog process for terminating the daemon process when the backend app terminates.
|
||||||
@ -132,12 +131,8 @@ export class ArduinoDaemonImpl
|
|||||||
return this.onDaemonStoppedEmitter.event;
|
return this.onDaemonStoppedEmitter.event;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getExecPath(): Promise<string> {
|
getExecPath(): string {
|
||||||
if (this._execPath) {
|
return getExecPath('arduino-cli');
|
||||||
return this._execPath;
|
|
||||||
}
|
|
||||||
this._execPath = await getExecPath('arduino-cli', this.onError.bind(this));
|
|
||||||
return this._execPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getSpawnArgs(): Promise<string[]> {
|
protected async getSpawnArgs(): Promise<string[]> {
|
||||||
@ -151,7 +146,7 @@ export class ArduinoDaemonImpl
|
|||||||
'--port',
|
'--port',
|
||||||
'0',
|
'0',
|
||||||
'--config-file',
|
'--config-file',
|
||||||
`"${cliConfigPath}"`,
|
cliConfigPath,
|
||||||
'-v',
|
'-v',
|
||||||
];
|
];
|
||||||
if (debug) {
|
if (debug) {
|
||||||
@ -173,10 +168,8 @@ export class ArduinoDaemonImpl
|
|||||||
daemon: ChildProcess;
|
daemon: ChildProcess;
|
||||||
port: string;
|
port: string;
|
||||||
}> {
|
}> {
|
||||||
const [cliPath, args] = await Promise.all([
|
const args = await this.getSpawnArgs();
|
||||||
this.getExecPath(),
|
const cliPath = this.getExecPath();
|
||||||
this.getSpawnArgs(),
|
|
||||||
]);
|
|
||||||
const ready = new Deferred<{ daemon: ChildProcess; port: string }>();
|
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);
|
||||||
|
@ -1,45 +1,22 @@
|
|||||||
|
import { ILogger } from '@theia/core/lib/common/logger';
|
||||||
|
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||||
|
import type { Port } from '../common/protocol';
|
||||||
import {
|
import {
|
||||||
ArduinoFirmwareUploader,
|
ArduinoFirmwareUploader,
|
||||||
FirmwareInfo,
|
FirmwareInfo,
|
||||||
} from '../common/protocol/arduino-firmware-uploader';
|
} from '../common/protocol/arduino-firmware-uploader';
|
||||||
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
|
||||||
import { ExecutableService, Port } from '../common/protocol';
|
|
||||||
import { getExecPath, spawnCommand } from './exec-util';
|
import { getExecPath, spawnCommand } from './exec-util';
|
||||||
import { ILogger } from '@theia/core/lib/common/logger';
|
|
||||||
import { MonitorManager } from './monitor-manager';
|
import { MonitorManager } from './monitor-manager';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
|
export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
|
||||||
@inject(ExecutableService)
|
|
||||||
protected executableService: ExecutableService;
|
|
||||||
|
|
||||||
protected _execPath: string | undefined;
|
|
||||||
|
|
||||||
@inject(ILogger)
|
@inject(ILogger)
|
||||||
@named('fwuploader')
|
@named('fwuploader')
|
||||||
protected readonly logger: ILogger;
|
private readonly logger: ILogger;
|
||||||
|
|
||||||
@inject(MonitorManager)
|
@inject(MonitorManager)
|
||||||
protected readonly monitorManager: MonitorManager;
|
private readonly monitorManager: MonitorManager;
|
||||||
|
|
||||||
protected onError(error: any): void {
|
async uploadCertificates(command: string): Promise<string> {
|
||||||
this.logger.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getExecPath(): Promise<string> {
|
|
||||||
if (this._execPath) {
|
|
||||||
return this._execPath;
|
|
||||||
}
|
|
||||||
this._execPath = await getExecPath('arduino-fwuploader');
|
|
||||||
return this._execPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
async runCommand(args: string[]): Promise<any> {
|
|
||||||
const execPath = await this.getExecPath();
|
|
||||||
return await spawnCommand(`"${execPath}"`, args, this.onError.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
async uploadCertificates(command: string): Promise<any> {
|
|
||||||
return await this.runCommand(['certificates', 'flash', command]);
|
return await this.runCommand(['certificates', 'flash', command]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,14 +47,13 @@ export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async flash(firmware: FirmwareInfo, port: Port): Promise<string> {
|
async flash(firmware: FirmwareInfo, port: Port): Promise<string> {
|
||||||
let output;
|
|
||||||
const board = {
|
const board = {
|
||||||
name: firmware.board_name,
|
name: firmware.board_name,
|
||||||
fqbn: firmware.board_fqbn,
|
fqbn: firmware.board_fqbn,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await this.monitorManager.notifyUploadStarted(board.fqbn, port);
|
await this.monitorManager.notifyUploadStarted(board.fqbn, port);
|
||||||
output = await this.runCommand([
|
const output = await this.runCommand([
|
||||||
'firmware',
|
'firmware',
|
||||||
'flash',
|
'flash',
|
||||||
'--fqbn',
|
'--fqbn',
|
||||||
@ -87,11 +63,18 @@ export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
|
|||||||
'--module',
|
'--module',
|
||||||
`${firmware.module}@${firmware.firmware_version}`,
|
`${firmware.module}@${firmware.firmware_version}`,
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
return output;
|
||||||
throw e;
|
|
||||||
} finally {
|
} finally {
|
||||||
await this.monitorManager.notifyUploadFinished(board.fqbn, port);
|
await this.monitorManager.notifyUploadFinished(board.fqbn, port);
|
||||||
return output;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onError(error: Error): void {
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runCommand(args: string[]): Promise<string> {
|
||||||
|
const execPath = getExecPath('arduino-fwuploader');
|
||||||
|
return await spawnCommand(execPath, args, this.onError.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import * as os from 'node:os';
|
|
||||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||||
@ -15,7 +14,7 @@ export class ClangFormatter implements Formatter {
|
|||||||
private readonly configService: ConfigService;
|
private readonly configService: ConfigService;
|
||||||
|
|
||||||
@inject(EnvVariablesServer)
|
@inject(EnvVariablesServer)
|
||||||
private readonly envVariableServer: EnvVariablesServer;
|
private readonly envVariablesServer: EnvVariablesServer;
|
||||||
|
|
||||||
async format({
|
async format({
|
||||||
content,
|
content,
|
||||||
@ -26,26 +25,19 @@ export class ClangFormatter implements Formatter {
|
|||||||
formatterConfigFolderUris: string[];
|
formatterConfigFolderUris: string[];
|
||||||
options?: FormatterOptions;
|
options?: FormatterOptions;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const [execPath, style] = await Promise.all([
|
const execPath = this.execPath();
|
||||||
this.execPath(),
|
const args = await this.styleArgs(formatterConfigFolderUris, options);
|
||||||
this.style(formatterConfigFolderUris, options),
|
|
||||||
]);
|
|
||||||
const formatted = await spawnCommand(
|
const formatted = await spawnCommand(
|
||||||
`"${execPath}"`,
|
execPath,
|
||||||
[style],
|
args,
|
||||||
console.error,
|
console.error,
|
||||||
content
|
content
|
||||||
);
|
);
|
||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _execPath: string | undefined;
|
private execPath(): string {
|
||||||
private async execPath(): Promise<string> {
|
return getExecPath('clang-format');
|
||||||
if (this._execPath) {
|
|
||||||
return this._execPath;
|
|
||||||
}
|
|
||||||
this._execPath = await getExecPath('clang-format');
|
|
||||||
return this._execPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,10 +52,10 @@ export class ClangFormatter implements Formatter {
|
|||||||
*
|
*
|
||||||
* See: https://github.com/arduino/arduino-ide/issues/566
|
* See: https://github.com/arduino/arduino-ide/issues/566
|
||||||
*/
|
*/
|
||||||
private async style(
|
private async styleArgs(
|
||||||
formatterConfigFolderUris: string[],
|
formatterConfigFolderUris: string[],
|
||||||
options?: FormatterOptions
|
options?: FormatterOptions
|
||||||
): Promise<string> {
|
): Promise<string[]> {
|
||||||
const clangFormatPaths = await Promise.all([
|
const clangFormatPaths = await Promise.all([
|
||||||
...formatterConfigFolderUris.map((uri) => this.clangConfigPath(uri)),
|
...formatterConfigFolderUris.map((uri) => this.clangConfigPath(uri)),
|
||||||
this.clangConfigPath(this.configDirPath()),
|
this.clangConfigPath(this.configDirPath()),
|
||||||
@ -72,11 +64,11 @@ export class ClangFormatter implements Formatter {
|
|||||||
const first = clangFormatPaths.filter(Boolean).shift();
|
const first = clangFormatPaths.filter(Boolean).shift();
|
||||||
if (first) {
|
if (first) {
|
||||||
console.debug(
|
console.debug(
|
||||||
`Using ${ClangFormatFile} style configuration from '${first}'.`
|
`Using ${clangFormatFilename} style configuration from '${first}'.`
|
||||||
);
|
);
|
||||||
return `-style=file:"${first}"`;
|
return ['-style', `file:${first}`];
|
||||||
}
|
}
|
||||||
return `-style="${style(toClangOptions(options))}"`;
|
return ['-style', style(toClangOptions(options))];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async dataDirPath(): Promise<string | undefined> {
|
private async dataDirPath(): Promise<string | undefined> {
|
||||||
@ -88,7 +80,7 @@ export class ClangFormatter implements Formatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async configDirPath(): Promise<string> {
|
private async configDirPath(): Promise<string> {
|
||||||
const configDirUri = await this.envVariableServer.getConfigDirUri();
|
const configDirUri = await this.envVariablesServer.getConfigDirUri();
|
||||||
return FileUri.fsPath(configDirUri);
|
return FileUri.fsPath(configDirUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +92,7 @@ export class ClangFormatter implements Formatter {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const folderPath = FileUri.fsPath(uri);
|
const folderPath = FileUri.fsPath(uri);
|
||||||
const clangFormatPath = join(folderPath, ClangFormatFile);
|
const clangFormatPath = join(folderPath, clangFormatFilename);
|
||||||
try {
|
try {
|
||||||
await fs.access(clangFormatPath, constants.R_OK);
|
await fs.access(clangFormatPath, constants.R_OK);
|
||||||
return clangFormatPath;
|
return clangFormatPath;
|
||||||
@ -115,7 +107,7 @@ interface ClangFormatOptions {
|
|||||||
readonly TabWidth: number;
|
readonly TabWidth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClangFormatFile = '.clang-format';
|
export const clangFormatFilename = '.clang-format';
|
||||||
|
|
||||||
function toClangOptions(
|
function toClangOptions(
|
||||||
options?: FormatterOptions | undefined
|
options?: FormatterOptions | undefined
|
||||||
@ -129,24 +121,8 @@ function toClangOptions(
|
|||||||
return { UseTab: 'Never', TabWidth: 2 };
|
return { UseTab: 'Never', TabWidth: 2 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function style({ TabWidth, UseTab }: ClangFormatOptions): string {
|
function style({ TabWidth, UseTab }: ClangFormatOptions): string {
|
||||||
let styleArgument = JSON.stringify(styleJson({ TabWidth, UseTab })).replace(
|
return JSON.stringify(styleJson({ TabWidth, UseTab }));
|
||||||
/[\\"]/g,
|
|
||||||
'\\$&'
|
|
||||||
);
|
|
||||||
if (os.platform() === 'win32') {
|
|
||||||
// Windows command interpreter does not use backslash escapes. This causes the argument to have alternate quoted and
|
|
||||||
// unquoted sections.
|
|
||||||
// Special characters in the unquoted sections must be caret escaped.
|
|
||||||
const styleArgumentSplit = styleArgument.split('"');
|
|
||||||
for (let i = 1; i < styleArgumentSplit.length; i += 2) {
|
|
||||||
styleArgumentSplit[i] = styleArgumentSplit[i].replace(/[<>^|]/g, '^$&');
|
|
||||||
}
|
|
||||||
|
|
||||||
styleArgument = styleArgumentSplit.join('"');
|
|
||||||
}
|
|
||||||
|
|
||||||
return styleArgument;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function styleJson({
|
function styleJson({
|
||||||
|
@ -222,8 +222,8 @@ export class ConfigServiceImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getFallbackCliConfig(): Promise<DefaultCliConfig> {
|
private async getFallbackCliConfig(): Promise<DefaultCliConfig> {
|
||||||
const cliPath = await this.daemon.getExecPath();
|
const cliPath = this.daemon.getExecPath();
|
||||||
const rawJson = await spawnCommand(`"${cliPath}"`, [
|
const rawJson = await spawnCommand(cliPath, [
|
||||||
'config',
|
'config',
|
||||||
'dump',
|
'dump',
|
||||||
'format',
|
'format',
|
||||||
@ -233,13 +233,8 @@ export class ConfigServiceImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async initCliConfigTo(fsPathToDir: string): Promise<void> {
|
private async initCliConfigTo(fsPathToDir: string): Promise<void> {
|
||||||
const cliPath = await this.daemon.getExecPath();
|
const cliPath = this.daemon.getExecPath();
|
||||||
await spawnCommand(`"${cliPath}"`, [
|
await spawnCommand(cliPath, ['config', 'init', '--dest-dir', fsPathToDir]);
|
||||||
'config',
|
|
||||||
'init',
|
|
||||||
'--dest-dir',
|
|
||||||
`"${fsPathToDir}"`,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async mapCliConfigToAppConfig(
|
private async mapCliConfigToAppConfig(
|
||||||
|
@ -1,51 +1,17 @@
|
|||||||
import os from 'node:os';
|
|
||||||
import which from 'which';
|
|
||||||
import semver from 'semver';
|
|
||||||
import { join } from 'node:path';
|
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
import os from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
export async function getExecPath(
|
export type ArduinoBinaryName =
|
||||||
commandName: string,
|
| 'arduino-cli'
|
||||||
onError: (error: Error) => void = (error) => console.log(error),
|
| 'arduino-fwuploader'
|
||||||
versionArg?: string | undefined,
|
| 'arduino-language-server';
|
||||||
inBinDir?: boolean
|
export type ClangBinaryName = 'clangd' | 'clang-format';
|
||||||
): Promise<string> {
|
export type BinaryName = ArduinoBinaryName | ClangBinaryName;
|
||||||
const execName = `${commandName}${os.platform() === 'win32' ? '.exe' : ''}`;
|
|
||||||
const relativePath = ['..', '..', 'build'];
|
export function getExecPath(binaryName: BinaryName): string {
|
||||||
if (inBinDir) {
|
const filename = `${binaryName}${os.platform() === 'win32' ? '.exe' : ''}`;
|
||||||
relativePath.push('bin');
|
return join(__dirname, '..', '..', 'build', filename);
|
||||||
}
|
|
||||||
const buildCommand = join(__dirname, ...relativePath, execName);
|
|
||||||
if (!versionArg) {
|
|
||||||
return buildCommand;
|
|
||||||
}
|
|
||||||
const versionRegexp = /\d+\.\d+\.\d+/;
|
|
||||||
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))
|
|
||||||
);
|
|
||||||
if (!pathCommand) {
|
|
||||||
return buildCommand;
|
|
||||||
}
|
|
||||||
const pathVersion = await spawnCommand(
|
|
||||||
`"${pathCommand}"`,
|
|
||||||
[versionArg],
|
|
||||||
onError
|
|
||||||
);
|
|
||||||
const pathShortVersion = (pathVersion.match(versionRegexp) || [])[0];
|
|
||||||
if (
|
|
||||||
pathShortVersion &&
|
|
||||||
buildShortVersion &&
|
|
||||||
semver.gt(pathShortVersion, buildShortVersion)
|
|
||||||
) {
|
|
||||||
return pathCommand;
|
|
||||||
}
|
|
||||||
return buildCommand;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spawnCommand(
|
export function spawnCommand(
|
||||||
@ -55,7 +21,7 @@ export function spawnCommand(
|
|||||||
stdIn?: string
|
stdIn?: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
const cp = spawn(command, args, { windowsHide: true, shell: true });
|
const cp = spawn(command, args, { windowsHide: true });
|
||||||
const outBuffers: Buffer[] = [];
|
const outBuffers: Buffer[] = [];
|
||||||
const errBuffers: Buffer[] = [];
|
const errBuffers: Buffer[] = [];
|
||||||
cp.stdout.on('data', (b: Buffer) => outBuffers.push(b));
|
cp.stdout.on('data', (b: Buffer) => outBuffers.push(b));
|
||||||
|
@ -1,35 +1,19 @@
|
|||||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
|
||||||
import { ILogger } from '@theia/core/lib/common/logger';
|
|
||||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||||
import { getExecPath } from './exec-util';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { ExecutableService } from '../common/protocol/executable-service';
|
import { ExecutableService } from '../common/protocol/executable-service';
|
||||||
|
import { getExecPath } from './exec-util';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class ExecutableServiceImpl implements ExecutableService {
|
export class ExecutableServiceImpl implements ExecutableService {
|
||||||
@inject(ILogger)
|
|
||||||
protected logger: ILogger;
|
|
||||||
|
|
||||||
async list(): Promise<{
|
async list(): Promise<{
|
||||||
clangdUri: string;
|
clangdUri: string;
|
||||||
cliUri: string;
|
cliUri: string;
|
||||||
lsUri: string;
|
lsUri: string;
|
||||||
fwuploaderUri: string;
|
|
||||||
}> {
|
}> {
|
||||||
const [ls, clangd, cli, fwuploader] = await Promise.all([
|
|
||||||
getExecPath('arduino-language-server', this.onError.bind(this)),
|
|
||||||
getExecPath('clangd', this.onError.bind(this), undefined),
|
|
||||||
getExecPath('arduino-cli', this.onError.bind(this)),
|
|
||||||
getExecPath('arduino-fwuploader', this.onError.bind(this)),
|
|
||||||
]);
|
|
||||||
return {
|
return {
|
||||||
clangdUri: FileUri.create(clangd).toString(),
|
clangdUri: FileUri.create(getExecPath('clangd')).toString(),
|
||||||
cliUri: FileUri.create(cli).toString(),
|
cliUri: FileUri.create(getExecPath('arduino-cli')).toString(),
|
||||||
lsUri: FileUri.create(ls).toString(),
|
lsUri: FileUri.create(getExecPath('arduino-language-server')).toString(),
|
||||||
fwuploaderUri: FileUri.create(fwuploader).toString(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onError(error: Error): void {
|
|
||||||
this.logger.error(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -43,19 +43,13 @@ class SilentArduinoDaemonImpl extends ArduinoDaemonImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async initCliConfig(): Promise<string> {
|
private async initCliConfig(): Promise<string> {
|
||||||
const cliPath = await this.getExecPath();
|
const cliPath = this.getExecPath();
|
||||||
const destDir = track.mkdirSync();
|
const destDir = track.mkdirSync();
|
||||||
await spawnCommand(`"${cliPath}"`, [
|
await spawnCommand(cliPath, ['config', 'init', '--dest-dir', destDir]);
|
||||||
'config',
|
|
||||||
'init',
|
|
||||||
'--dest-dir',
|
|
||||||
destDir,
|
|
||||||
]);
|
|
||||||
const content = fs.readFileSync(path.join(destDir, CLI_CONFIG), {
|
const content = fs.readFileSync(path.join(destDir, CLI_CONFIG), {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
});
|
});
|
||||||
const cliConfig = safeLoad(content) as any;
|
const cliConfig = safeLoad(content);
|
||||||
// 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',
|
||||||
|
162
arduino-ide-extension/src/test/node/clang-formatter.test.ts
Normal file
162
arduino-ide-extension/src/test/node/clang-formatter.test.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import {
|
||||||
|
Disposable,
|
||||||
|
DisposableCollection,
|
||||||
|
} from '@theia/core/lib/common/disposable';
|
||||||
|
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { promises as fs } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import temp from 'temp';
|
||||||
|
import {
|
||||||
|
clangFormatFilename,
|
||||||
|
ClangFormatter,
|
||||||
|
} from '../../node/clang-formatter';
|
||||||
|
import { spawnCommand } from '../../node/exec-util';
|
||||||
|
import { createBaseContainer, startDaemon } from './test-bindings';
|
||||||
|
|
||||||
|
const unformattedContent = `void setup ( ) { pinMode(LED_BUILTIN, OUTPUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
digitalWrite( LED_BUILTIN , HIGH );
|
||||||
|
delay( 1000 ) ;
|
||||||
|
digitalWrite( LED_BUILTIN , LOW);
|
||||||
|
delay ( 1000 ) ;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const formattedContent = `void setup() {
|
||||||
|
pinMode(LED_BUILTIN, OUTPUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
digitalWrite(LED_BUILTIN, HIGH);
|
||||||
|
delay(1000);
|
||||||
|
digitalWrite(LED_BUILTIN, LOW);
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ClangStyleValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| ClangStyleValue[]
|
||||||
|
| { [key: string]: ClangStyleValue };
|
||||||
|
type ClangConfiguration = Record<string, ClangStyleValue>;
|
||||||
|
|
||||||
|
export interface ClangStyle {
|
||||||
|
readonly key: string;
|
||||||
|
readonly value: ClangStyleValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleClangStyles: ClangStyle[] = [
|
||||||
|
{
|
||||||
|
key: 'SpacesBeforeTrailingComments',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'SortIncludes',
|
||||||
|
value: 'Never',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'AlignTrailingComments',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'IfMacros',
|
||||||
|
value: ['KJ_IF_MAYBE'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'SpacesInLineCommentPrefix',
|
||||||
|
value: {
|
||||||
|
Minimum: 0,
|
||||||
|
Maximum: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function expectNoChanges(
|
||||||
|
formatter: ClangFormatter,
|
||||||
|
styleArg: string
|
||||||
|
): Promise<void> {
|
||||||
|
const minimalContent = `
|
||||||
|
void setup() {}
|
||||||
|
void loop() {}
|
||||||
|
`.trim();
|
||||||
|
const execPath = formatter['execPath']();
|
||||||
|
const actual = await spawnCommand(
|
||||||
|
execPath,
|
||||||
|
['-style', styleArg],
|
||||||
|
console.error,
|
||||||
|
minimalContent
|
||||||
|
);
|
||||||
|
expect(actual).to.be.equal(minimalContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('clang-formatter', () => {
|
||||||
|
let tracked: typeof temp;
|
||||||
|
let formatter: ClangFormatter;
|
||||||
|
let toDispose: DisposableCollection;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
tracked = temp.track();
|
||||||
|
toDispose = new DisposableCollection(
|
||||||
|
Disposable.create(() => tracked.cleanupSync())
|
||||||
|
);
|
||||||
|
const container = await createBaseContainer({
|
||||||
|
additionalBindings: (bind) =>
|
||||||
|
bind(ClangFormatter).toSelf().inSingletonScope(),
|
||||||
|
});
|
||||||
|
await startDaemon(container, toDispose);
|
||||||
|
formatter = container.get<ClangFormatter>(ClangFormatter);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => toDispose.dispose());
|
||||||
|
|
||||||
|
singleClangStyles
|
||||||
|
.map((style) => ({
|
||||||
|
...style,
|
||||||
|
styleArg: JSON.stringify({ [style.key]: style.value }),
|
||||||
|
}))
|
||||||
|
.map(({ value, styleArg }) =>
|
||||||
|
it(`should execute the formatter with a single ${
|
||||||
|
Array.isArray(value) ? 'array' : typeof value
|
||||||
|
} type style configuration value: ${styleArg}`, async () => {
|
||||||
|
await expectNoChanges(formatter, styleArg);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should execute the formatter with a multiple clang formatter styles', async () => {
|
||||||
|
const styleArg = JSON.stringify(
|
||||||
|
singleClangStyles.reduce((config, curr) => {
|
||||||
|
config[curr.key] = curr.value;
|
||||||
|
return config;
|
||||||
|
}, {} as ClangConfiguration)
|
||||||
|
);
|
||||||
|
await expectNoChanges(formatter, styleArg);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format with the default styles', async () => {
|
||||||
|
const actual = await formatter.format({
|
||||||
|
content: unformattedContent,
|
||||||
|
formatterConfigFolderUris: [],
|
||||||
|
});
|
||||||
|
expect(actual).to.be.equal(formattedContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format with custom formatter configuration file', async () => {
|
||||||
|
const tempPath = tracked.mkdirSync();
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(tempPath, clangFormatFilename),
|
||||||
|
'SpaceInEmptyParentheses: true',
|
||||||
|
{
|
||||||
|
encoding: 'utf8',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const actual = await formatter.format({
|
||||||
|
content: 'void foo() {}',
|
||||||
|
formatterConfigFolderUris: [FileUri.create(tempPath).toString()],
|
||||||
|
});
|
||||||
|
expect(actual).to.be.equal('void foo( ) {}');
|
||||||
|
});
|
||||||
|
});
|
@ -1,33 +1,164 @@
|
|||||||
import * as os from 'node:os';
|
import { assert, expect } from 'chai';
|
||||||
import { expect, use } from 'chai';
|
import fs from 'node:fs';
|
||||||
import { getExecPath } from '../../node/exec-util';
|
import path from 'node:path';
|
||||||
|
import {
|
||||||
|
ArduinoBinaryName,
|
||||||
|
BinaryName,
|
||||||
|
ClangBinaryName,
|
||||||
|
getExecPath,
|
||||||
|
spawnCommand,
|
||||||
|
} from '../../node/exec-util';
|
||||||
|
import temp from 'temp';
|
||||||
|
|
||||||
use(require('chai-string'));
|
describe('exec-utils', () => {
|
||||||
|
describe('spawnCommand', () => {
|
||||||
|
let tracked: typeof temp;
|
||||||
|
|
||||||
describe('getExecPath', () => {
|
before(() => {
|
||||||
it('should resolve arduino-cli', async () => {
|
tracked = temp.track();
|
||||||
const actual = await getExecPath('arduino-cli', onError, 'version');
|
});
|
||||||
const expected =
|
|
||||||
os.platform() === 'win32' ? '\\arduino-cli.exe' : '/arduino-cli';
|
after(() => {
|
||||||
expect(actual).to.endsWith(expected);
|
if (tracked) {
|
||||||
|
tracked.cleanupSync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should execute the command without 'shell:true' even if the path contains spaces but is not escaped", async () => {
|
||||||
|
const segment = 'with some spaces';
|
||||||
|
const cliPath = getExecPath('arduino-cli');
|
||||||
|
const filename = path.basename(cliPath);
|
||||||
|
const tempPath = tracked.mkdirSync();
|
||||||
|
const tempPathWitSpaces = path.join(tempPath, segment);
|
||||||
|
fs.mkdirSync(tempPathWitSpaces, { recursive: true });
|
||||||
|
const cliCopyPath = path.join(tempPathWitSpaces, filename);
|
||||||
|
fs.copyFileSync(cliPath, cliCopyPath);
|
||||||
|
expect(fs.accessSync(cliCopyPath, fs.constants.X_OK)).to.be.undefined;
|
||||||
|
expect(cliCopyPath.includes(segment)).to.be.true;
|
||||||
|
const stdout = await spawnCommand(cliCopyPath, ['version']);
|
||||||
|
expect(stdout.includes(filename)).to.be.true;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve arduino-language-server', async () => {
|
describe('getExecPath', () => {
|
||||||
const actual = await getExecPath('arduino-language-server');
|
type AssertOutput = (stdout: string) => void;
|
||||||
const expected =
|
|
||||||
os.platform() === 'win32'
|
|
||||||
? '\\arduino-language-server.exe'
|
|
||||||
: '/arduino-language-server';
|
|
||||||
expect(actual).to.endsWith(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should resolve clangd', async () => {
|
interface GetExecPathTestSuite {
|
||||||
const actual = await getExecPath('clangd', onError, '--version');
|
readonly name: BinaryName;
|
||||||
const expected = os.platform() === 'win32' ? '\\clangd.exe' : '/clangd';
|
readonly flags?: string[];
|
||||||
expect(actual).to.endsWith(expected);
|
readonly assertOutput: AssertOutput;
|
||||||
});
|
/**
|
||||||
|
* The Arduino LS repository is not as shiny as the CLI or the firmware uploader.
|
||||||
|
* It does not support `version` flag either, so non-zero exit is expected.
|
||||||
|
*/
|
||||||
|
readonly expectNonZeroExit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
function onError(error: Error): void {
|
const binaryNameToVersionMapping: Record<BinaryName, string> = {
|
||||||
console.error(error);
|
'arduino-cli': 'cli',
|
||||||
}
|
'arduino-language-server': 'languageServer',
|
||||||
|
'arduino-fwuploader': 'fwuploader',
|
||||||
|
clangd: 'clangd',
|
||||||
|
'clang-format': 'clangd',
|
||||||
|
};
|
||||||
|
|
||||||
|
function readVersionFromPackageJson(name: BinaryName): string {
|
||||||
|
const raw = fs.readFileSync(
|
||||||
|
path.join(__dirname, '..', '..', '..', 'package.json'),
|
||||||
|
{ encoding: 'utf8' }
|
||||||
|
);
|
||||||
|
const json = JSON.parse(raw);
|
||||||
|
expect(json.arduino).to.be.not.undefined;
|
||||||
|
const mappedName = binaryNameToVersionMapping[name];
|
||||||
|
expect(mappedName).to.be.not.undefined;
|
||||||
|
const version = json.arduino[mappedName].version;
|
||||||
|
expect(version).to.be.not.undefined;
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTaskAssert(name: ArduinoBinaryName): AssertOutput {
|
||||||
|
const version = readVersionFromPackageJson(name);
|
||||||
|
if (typeof version === 'string') {
|
||||||
|
return (stdout: string) => {
|
||||||
|
expect(stdout.includes(name)).to.be.true;
|
||||||
|
expect(stdout.includes(version)).to.be.true;
|
||||||
|
expect(stdout.includes('git-snapshot')).to.be.false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return (stdout: string) => {
|
||||||
|
expect(stdout.includes(name)).to.be.true;
|
||||||
|
expect(stdout.includes('git-snapshot')).to.be.true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createClangdAssert(name: ClangBinaryName): AssertOutput {
|
||||||
|
const version = readVersionFromPackageJson(name);
|
||||||
|
return (stdout: string) => {
|
||||||
|
expect(stdout.includes(name)).to.be.true;
|
||||||
|
expect(stdout.includes(`version ${version}`)).to.be.true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const suites: GetExecPathTestSuite[] = [
|
||||||
|
{
|
||||||
|
name: 'arduino-cli',
|
||||||
|
flags: ['version'],
|
||||||
|
assertOutput: createTaskAssert('arduino-cli'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'arduino-fwuploader',
|
||||||
|
flags: ['version'],
|
||||||
|
assertOutput: createTaskAssert('arduino-fwuploader'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'arduino-language-server',
|
||||||
|
assertOutput: (stderr: string) => {
|
||||||
|
expect(stderr.includes('Path to ArduinoCLI config file must be set.'))
|
||||||
|
.to.be.true;
|
||||||
|
},
|
||||||
|
expectNonZeroExit: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'clangd',
|
||||||
|
flags: ['--version'],
|
||||||
|
assertOutput: createClangdAssert('clangd'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'clang-format',
|
||||||
|
flags: ['--version'],
|
||||||
|
assertOutput: createClangdAssert('clang-format'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// This is not a functional test but it ensures all executables provided by IDE2 are tested.
|
||||||
|
it('should cover all provided executables', () => {
|
||||||
|
expect(suites.length).to.be.equal(
|
||||||
|
Object.keys(binaryNameToVersionMapping).length
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
suites.map((suite) =>
|
||||||
|
it(`should resolve '${suite.name}'`, async () => {
|
||||||
|
const execPath = getExecPath(suite.name);
|
||||||
|
expect(execPath).to.be.not.undefined;
|
||||||
|
expect(execPath).to.be.not.empty;
|
||||||
|
expect(fs.accessSync(execPath, fs.constants.X_OK)).to.be.undefined;
|
||||||
|
if (suite.expectNonZeroExit) {
|
||||||
|
try {
|
||||||
|
await spawnCommand(execPath, suite.flags ?? []);
|
||||||
|
assert.fail('Expected a non-zero exit code');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceOf(Error);
|
||||||
|
const stderr = (<Error>err).message;
|
||||||
|
expect(stderr).to.be.not.undefined;
|
||||||
|
expect(stderr).to.be.not.empty;
|
||||||
|
suite.assertOutput(stderr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const stdout = await spawnCommand(execPath, suite.flags ?? []);
|
||||||
|
suite.assertOutput(stdout);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
16
yarn.lock
16
yarn.lock
@ -2741,14 +2741,7 @@
|
|||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
"@types/responselike" "^1.0.0"
|
"@types/responselike" "^1.0.0"
|
||||||
|
|
||||||
"@types/chai-string@^1.4.2":
|
"@types/chai@^4.2.7":
|
||||||
version "1.4.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/chai-string/-/chai-string-1.4.2.tgz#0f116504a666b6c6a3c42becf86634316c9a19ac"
|
|
||||||
integrity sha512-ld/1hV5qcPRGuwlPdvRfvM3Ka/iofOk2pH4VkasK4b1JJP1LjNmWWn0LsISf6RRzyhVOvs93rb9tM09e+UuF8Q==
|
|
||||||
dependencies:
|
|
||||||
"@types/chai" "*"
|
|
||||||
|
|
||||||
"@types/chai@*", "@types/chai@^4.2.7":
|
|
||||||
version "4.3.4"
|
version "4.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4"
|
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4"
|
||||||
integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==
|
integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==
|
||||||
@ -4814,11 +4807,6 @@ caw@^2.0.1:
|
|||||||
tunnel-agent "^0.6.0"
|
tunnel-agent "^0.6.0"
|
||||||
url-to-options "^1.0.1"
|
url-to-options "^1.0.1"
|
||||||
|
|
||||||
chai-string@^1.5.0:
|
|
||||||
version "1.5.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/chai-string/-/chai-string-1.5.0.tgz#0bdb2d8a5f1dbe90bc78ec493c1c1c180dd4d3d2"
|
|
||||||
integrity sha512-sydDC3S3pNAQMYwJrs6dQX0oBQ6KfIPuOZ78n7rocW0eJJlsHPh2t3kwW7xfwYA/1Bf6/arGtSUo16rxR2JFlw==
|
|
||||||
|
|
||||||
chai@^4.2.0:
|
chai@^4.2.0:
|
||||||
version "4.3.7"
|
version "4.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51"
|
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51"
|
||||||
@ -15188,7 +15176,7 @@ which-typed-array@^1.1.2, which-typed-array@^1.1.9:
|
|||||||
has-tostringtag "^1.0.0"
|
has-tostringtag "^1.0.0"
|
||||||
is-typed-array "^1.1.10"
|
is-typed-array "^1.1.10"
|
||||||
|
|
||||||
which@1.3.1, which@^1.2.9, which@^1.3.1:
|
which@1.3.1, which@^1.2.9:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||||
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
|
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user