mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-12 22:06:34 +00:00
fix: jsonc parsing in the IDE2 backend
Occurred when `settings.json` contained comments or a trailing comma. Closes #1945 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
24dc0bbc88
commit
fb10de1446
@ -78,6 +78,7 @@
|
|||||||
"google-protobuf": "^3.20.1",
|
"google-protobuf": "^3.20.1",
|
||||||
"hash.js": "^1.1.7",
|
"hash.js": "^1.1.7",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
|
"jsonc-parser": "^2.2.0",
|
||||||
"just-diff": "^5.1.1",
|
"just-diff": "^5.1.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"keytar": "7.2.0",
|
"keytar": "7.2.0",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||||
import { spawn, ChildProcess } from 'child_process';
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||||
@ -16,7 +15,7 @@ import { BackendApplicationContribution } from '@theia/core/lib/node/backend-app
|
|||||||
import { ArduinoDaemon, NotificationServiceServer } from '../common/protocol';
|
import { ArduinoDaemon, NotificationServiceServer } from '../common/protocol';
|
||||||
import { CLI_CONFIG } from './cli-config';
|
import { CLI_CONFIG } from './cli-config';
|
||||||
import { getExecPath } from './exec-util';
|
import { getExecPath } from './exec-util';
|
||||||
import { ErrnoException } from './utils/errors';
|
import { SettingsReader } from './settings-reader';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class ArduinoDaemonImpl
|
export class ArduinoDaemonImpl
|
||||||
@ -32,6 +31,9 @@ export class ArduinoDaemonImpl
|
|||||||
@inject(NotificationServiceServer)
|
@inject(NotificationServiceServer)
|
||||||
private readonly notificationService: NotificationServiceServer;
|
private readonly notificationService: NotificationServiceServer;
|
||||||
|
|
||||||
|
@inject(SettingsReader)
|
||||||
|
private readonly settingsReader: SettingsReader;
|
||||||
|
|
||||||
private readonly toDispose = new DisposableCollection();
|
private readonly toDispose = new DisposableCollection();
|
||||||
private readonly onDaemonStartedEmitter = new Emitter<string>();
|
private readonly onDaemonStartedEmitter = new Emitter<string>();
|
||||||
private readonly onDaemonStoppedEmitter = new Emitter<void>();
|
private readonly onDaemonStoppedEmitter = new Emitter<void>();
|
||||||
@ -149,34 +151,12 @@ export class ArduinoDaemonImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async debugDaemon(): Promise<boolean> {
|
private async debugDaemon(): Promise<boolean> {
|
||||||
// Poor man's preferences on the backend. (https://github.com/arduino/arduino-ide/issues/1056#issuecomment-1153975064)
|
const settings = await this.settingsReader.read();
|
||||||
const configDirUri = await this.envVariablesServer.getConfigDirUri();
|
if (settings) {
|
||||||
const configDirPath = FileUri.fsPath(configDirUri);
|
const value = settings['arduino.cli.daemon.debug'];
|
||||||
try {
|
return value === true;
|
||||||
const raw = await fs.readFile(join(configDirPath, 'settings.json'), {
|
|
||||||
encoding: 'utf8',
|
|
||||||
});
|
|
||||||
const json = this.tryParse(raw);
|
|
||||||
if (json) {
|
|
||||||
const value = json['arduino.cli.daemon.debug'];
|
|
||||||
return typeof value === 'boolean' && !!value;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
|
||||||
if (ErrnoException.isENOENT(error)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
private tryParse(raw: string): any | undefined {
|
|
||||||
try {
|
|
||||||
return JSON.parse(raw);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async spawnDaemonProcess(): Promise<{
|
protected async spawnDaemonProcess(): Promise<{
|
||||||
|
@ -118,6 +118,7 @@ import {
|
|||||||
LocalDirectoryPluginDeployerResolverWithFallback,
|
LocalDirectoryPluginDeployerResolverWithFallback,
|
||||||
PluginDeployer_GH_12064,
|
PluginDeployer_GH_12064,
|
||||||
} from './theia/plugin-ext/plugin-deployer';
|
} from './theia/plugin-ext/plugin-deployer';
|
||||||
|
import { SettingsReader } from './settings-reader';
|
||||||
|
|
||||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||||
bind(BackendApplication).toSelf().inSingletonScope();
|
bind(BackendApplication).toSelf().inSingletonScope();
|
||||||
@ -403,6 +404,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
.toSelf()
|
.toSelf()
|
||||||
.inSingletonScope();
|
.inSingletonScope();
|
||||||
rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope();
|
rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope();
|
||||||
|
|
||||||
|
bind(SettingsReader).toSelf().inSingletonScope();
|
||||||
});
|
});
|
||||||
|
|
||||||
function bindChildLogger(bind: interfaces.Bind, name: string): void {
|
function bindChildLogger(bind: interfaces.Bind, name: string): void {
|
||||||
|
53
arduino-ide-extension/src/node/settings-reader.ts
Normal file
53
arduino-ide-extension/src/node/settings-reader.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||||
|
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import {
|
||||||
|
parse as parseJsonc,
|
||||||
|
ParseError,
|
||||||
|
printParseErrorCode,
|
||||||
|
} from 'jsonc-parser';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { ErrnoException } from './utils/errors';
|
||||||
|
|
||||||
|
// Poor man's preferences on the backend. (https://github.com/arduino/arduino-ide/issues/1056#issuecomment-1153975064)
|
||||||
|
@injectable()
|
||||||
|
export class SettingsReader {
|
||||||
|
@inject(EnvVariablesServer)
|
||||||
|
private readonly envVariableServer: EnvVariablesServer;
|
||||||
|
|
||||||
|
async read(): Promise<Record<string, unknown> | undefined> {
|
||||||
|
const configDirUri = await this.envVariableServer.getConfigDirUri();
|
||||||
|
const configDirPath = FileUri.fsPath(configDirUri);
|
||||||
|
const settingsPath = join(configDirPath, 'settings.json');
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(settingsPath, { encoding: 'utf8' });
|
||||||
|
return parse(raw);
|
||||||
|
} catch (err) {
|
||||||
|
if (ErrnoException.isENOENT(err)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parse(raw: string): Record<string, unknown> | undefined {
|
||||||
|
const errors: ParseError[] = [];
|
||||||
|
const settings =
|
||||||
|
parseJsonc(raw, errors, {
|
||||||
|
allowEmptyContent: true,
|
||||||
|
allowTrailingComma: true,
|
||||||
|
disallowComments: false,
|
||||||
|
}) ?? {};
|
||||||
|
if (errors.length) {
|
||||||
|
console.error('Detected JSONC parser errors:');
|
||||||
|
console.error('----- CONTENT START -----');
|
||||||
|
console.error(raw);
|
||||||
|
console.error('----- CONTENT END -----');
|
||||||
|
errors.forEach(({ error, offset }) =>
|
||||||
|
console.error(` - ${printParseErrorCode(error)} at ${offset}`)
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return typeof settings === 'object' ? settings : undefined;
|
||||||
|
}
|
@ -43,6 +43,7 @@ import {
|
|||||||
firstToUpperCase,
|
firstToUpperCase,
|
||||||
startsWithUpperCase,
|
startsWithUpperCase,
|
||||||
} from '../common/utils';
|
} from '../common/utils';
|
||||||
|
import { SettingsReader } from './settings-reader';
|
||||||
|
|
||||||
const RecentSketches = 'recent-sketches.json';
|
const RecentSketches = 'recent-sketches.json';
|
||||||
const DefaultIno = `void setup() {
|
const DefaultIno = `void setup() {
|
||||||
@ -86,6 +87,9 @@ export class SketchesServiceImpl
|
|||||||
@inject(IsTempSketch)
|
@inject(IsTempSketch)
|
||||||
private readonly isTempSketch: IsTempSketch;
|
private readonly isTempSketch: IsTempSketch;
|
||||||
|
|
||||||
|
@inject(SettingsReader)
|
||||||
|
private readonly settingsReader: SettingsReader;
|
||||||
|
|
||||||
async getSketches({ uri }: { uri?: string }): Promise<SketchContainer> {
|
async getSketches({ uri }: { uri?: string }): Promise<SketchContainer> {
|
||||||
const root = await this.root(uri);
|
const root = await this.root(uri);
|
||||||
if (!root) {
|
if (!root) {
|
||||||
@ -631,38 +635,11 @@ export class SketchesServiceImpl
|
|||||||
return crypto.createHash('md5').update(path).digest('hex').toUpperCase();
|
return crypto.createHash('md5').update(path).digest('hex').toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the default.ino from the settings or from default folder.
|
|
||||||
private async readSettings(): Promise<Record<string, unknown> | undefined> {
|
|
||||||
const configDirUri = await this.envVariableServer.getConfigDirUri();
|
|
||||||
const configDirPath = FileUri.fsPath(configDirUri);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const raw = await fs.readFile(join(configDirPath, 'settings.json'), {
|
|
||||||
encoding: 'utf8',
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.tryParse(raw);
|
|
||||||
} catch (err) {
|
|
||||||
if (ErrnoException.isENOENT(err)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private tryParse(raw: string): Record<string, unknown> | undefined {
|
|
||||||
try {
|
|
||||||
return JSON.parse(raw);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the default.ino from the settings or from default folder.
|
// Returns the default.ino from the settings or from default folder.
|
||||||
private async loadInoContent(): Promise<string> {
|
private async loadInoContent(): Promise<string> {
|
||||||
if (!this.inoContent) {
|
if (!this.inoContent) {
|
||||||
this.inoContent = new Deferred<string>();
|
this.inoContent = new Deferred<string>();
|
||||||
const settings = await this.readSettings();
|
const settings = await this.settingsReader.read();
|
||||||
if (settings) {
|
if (settings) {
|
||||||
const inoBlueprintPath = settings['arduino.sketch.inoBlueprint'];
|
const inoBlueprintPath = settings['arduino.sketch.inoBlueprint'];
|
||||||
if (inoBlueprintPath && typeof inoBlueprintPath === 'string') {
|
if (inoBlueprintPath && typeof inoBlueprintPath === 'string') {
|
||||||
|
45
arduino-ide-extension/src/test/node/settings.reader.test.ts
Normal file
45
arduino-ide-extension/src/test/node/settings.reader.test.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
import { parse } from '../../node/settings-reader';
|
||||||
|
|
||||||
|
describe('settings-reader', () => {
|
||||||
|
describe('parse', () => {
|
||||||
|
it('should handle comments', () => {
|
||||||
|
const actual = parse(`
|
||||||
|
{
|
||||||
|
"alma": "korte",
|
||||||
|
// comment
|
||||||
|
"szilva": false
|
||||||
|
}`);
|
||||||
|
expect(actual).to.be.deep.equal({
|
||||||
|
alma: 'korte',
|
||||||
|
szilva: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle trailing comma', () => {
|
||||||
|
const actual = parse(`
|
||||||
|
{
|
||||||
|
"alma": "korte",
|
||||||
|
"szilva": 123,
|
||||||
|
}`);
|
||||||
|
expect(actual).to.be.deep.equal({
|
||||||
|
alma: 'korte',
|
||||||
|
szilva: 123,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse empty', () => {
|
||||||
|
const actual = parse('');
|
||||||
|
expect(actual).to.be.deep.equal({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse to undefined when parse has failed', () => {
|
||||||
|
const actual = parse(`
|
||||||
|
{
|
||||||
|
alma:: 'korte'
|
||||||
|
trash
|
||||||
|
}`);
|
||||||
|
expect(actual).to.be.undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -51,6 +51,7 @@ import {
|
|||||||
MonitorServiceFactory,
|
MonitorServiceFactory,
|
||||||
MonitorServiceFactoryOptions,
|
MonitorServiceFactoryOptions,
|
||||||
} from '../../node/monitor-service-factory';
|
} from '../../node/monitor-service-factory';
|
||||||
|
import { SettingsReader } from '../../node/settings-reader';
|
||||||
import { SketchesServiceImpl } from '../../node/sketches-service-impl';
|
import { SketchesServiceImpl } from '../../node/sketches-service-impl';
|
||||||
import { EnvVariablesServer } from '../../node/theia/env-variables/env-variables-server';
|
import { EnvVariablesServer } from '../../node/theia/env-variables/env-variables-server';
|
||||||
|
|
||||||
@ -277,6 +278,7 @@ export function createBaseContainer(
|
|||||||
bind(IsTempSketch).toSelf().inSingletonScope();
|
bind(IsTempSketch).toSelf().inSingletonScope();
|
||||||
bind(SketchesServiceImpl).toSelf().inSingletonScope();
|
bind(SketchesServiceImpl).toSelf().inSingletonScope();
|
||||||
bind(SketchesService).toService(SketchesServiceImpl);
|
bind(SketchesService).toService(SketchesServiceImpl);
|
||||||
|
bind(SettingsReader).toSelf().inSingletonScope();
|
||||||
if (containerCustomizations) {
|
if (containerCustomizations) {
|
||||||
containerCustomizations(bind, rebind);
|
containerCustomizations(bind, rebind);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user