mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-09 10:28:32 +00:00
feat: configure sketchbook location without restart
Closes #1764 Closes #796 Closes #569 Closes #655 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
Config,
|
||||
NotificationServiceServer,
|
||||
Network,
|
||||
ConfigState,
|
||||
} from '../common/protocol';
|
||||
import { spawnCommand } from './exec-util';
|
||||
import {
|
||||
@@ -25,7 +26,7 @@ import { ArduinoDaemonImpl } from './arduino-daemon-impl';
|
||||
import { DefaultCliConfig, CLI_CONFIG } from './cli-config';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { deepClone } from '@theia/core';
|
||||
import { deepClone, nls } from '@theia/core';
|
||||
import { ErrnoException } from './utils/errors';
|
||||
|
||||
const deepmerge = require('deepmerge');
|
||||
@@ -36,46 +37,38 @@ export class ConfigServiceImpl
|
||||
{
|
||||
@inject(ILogger)
|
||||
@named('config')
|
||||
protected readonly logger: ILogger;
|
||||
private readonly logger: ILogger;
|
||||
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariablesServer: EnvVariablesServer;
|
||||
private readonly envVariablesServer: EnvVariablesServer;
|
||||
|
||||
@inject(ArduinoDaemonImpl)
|
||||
protected readonly daemon: ArduinoDaemonImpl;
|
||||
private readonly daemon: ArduinoDaemonImpl;
|
||||
|
||||
@inject(NotificationServiceServer)
|
||||
protected readonly notificationService: NotificationServiceServer;
|
||||
private readonly notificationService: NotificationServiceServer;
|
||||
|
||||
protected config: Config;
|
||||
protected cliConfig: DefaultCliConfig | undefined;
|
||||
protected ready = new Deferred<void>();
|
||||
protected readonly configChangeEmitter = new Emitter<Config>();
|
||||
private config: ConfigState = {
|
||||
config: undefined,
|
||||
messages: ['uninitialized'],
|
||||
};
|
||||
private cliConfig: DefaultCliConfig | undefined;
|
||||
private ready = new Deferred<void>();
|
||||
private readonly configChangeEmitter = new Emitter<{
|
||||
oldState: ConfigState;
|
||||
newState: ConfigState;
|
||||
}>();
|
||||
|
||||
onStart(): void {
|
||||
this.loadCliConfig().then(async (cliConfig) => {
|
||||
this.cliConfig = cliConfig;
|
||||
if (this.cliConfig) {
|
||||
const [config] = await Promise.all([
|
||||
this.mapCliConfigToAppConfig(this.cliConfig),
|
||||
this.ensureUserDirExists(this.cliConfig),
|
||||
]);
|
||||
if (config) {
|
||||
this.config = config;
|
||||
this.ready.resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.fireInvalidConfig();
|
||||
});
|
||||
this.initConfig();
|
||||
}
|
||||
|
||||
async getCliConfigFileUri(): Promise<string> {
|
||||
private async getCliConfigFileUri(): Promise<string> {
|
||||
const configDirUri = await this.envVariablesServer.getConfigDirUri();
|
||||
return new URI(configDirUri).resolve(CLI_CONFIG).toString();
|
||||
}
|
||||
|
||||
async getConfiguration(): Promise<Config> {
|
||||
async getConfiguration(): Promise<ConfigState> {
|
||||
await this.ready.promise;
|
||||
return { ...this.config };
|
||||
}
|
||||
@@ -83,9 +76,10 @@ export class ConfigServiceImpl
|
||||
// Used by frontend to update the config.
|
||||
async setConfiguration(config: Config): Promise<void> {
|
||||
await this.ready.promise;
|
||||
if (Config.sameAs(this.config, config)) {
|
||||
if (Config.sameAs(this.config.config, config)) {
|
||||
return;
|
||||
}
|
||||
const oldConfigState = deepClone(this.config);
|
||||
let copyDefaultCliConfig: DefaultCliConfig | undefined = deepClone(
|
||||
this.cliConfig
|
||||
);
|
||||
@@ -110,16 +104,30 @@ export class ConfigServiceImpl
|
||||
await this.updateDaemon(port, copyDefaultCliConfig);
|
||||
await this.writeDaemonState(port);
|
||||
|
||||
this.config = deepClone(config);
|
||||
this.config.config = deepClone(config);
|
||||
this.cliConfig = copyDefaultCliConfig;
|
||||
this.fireConfigChanged(this.config);
|
||||
try {
|
||||
await this.validateCliConfig(this.cliConfig);
|
||||
delete this.config.messages;
|
||||
this.fireConfigChanged(oldConfigState, this.config);
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidConfigError) {
|
||||
this.config.messages = err.errors;
|
||||
this.fireConfigChanged(oldConfigState, this.config);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get cliConfiguration(): DefaultCliConfig | undefined {
|
||||
return this.cliConfig;
|
||||
}
|
||||
|
||||
get onConfigChange(): Event<Config> {
|
||||
get onConfigChange(): Event<{
|
||||
oldState: ConfigState;
|
||||
newState: ConfigState;
|
||||
}> {
|
||||
return this.configChangeEmitter.event;
|
||||
}
|
||||
|
||||
@@ -129,9 +137,42 @@ export class ConfigServiceImpl
|
||||
return this.daemon.getVersion();
|
||||
}
|
||||
|
||||
protected async loadCliConfig(
|
||||
private async initConfig(): Promise<void> {
|
||||
try {
|
||||
const cliConfig = await this.loadCliConfig();
|
||||
this.cliConfig = cliConfig;
|
||||
const [config] = await Promise.all([
|
||||
this.mapCliConfigToAppConfig(this.cliConfig),
|
||||
this.ensureUserDirExists(this.cliConfig).catch((reason) => {
|
||||
if (reason instanceof Error) {
|
||||
this.logger.warn(
|
||||
`Could not ensure user directory existence: ${this.cliConfig?.directories.user}`,
|
||||
reason
|
||||
);
|
||||
}
|
||||
// NOOP. Try to create the folder if missing but swallow any errors.
|
||||
// The validation will take care of the missing location handling.
|
||||
}),
|
||||
]);
|
||||
this.config.config = config;
|
||||
await this.validateCliConfig(this.cliConfig);
|
||||
delete this.config.messages;
|
||||
if (config) {
|
||||
this.ready.resolve();
|
||||
return;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
this.logger.error('Failed to initialize the CLI configuration.', err);
|
||||
if (err instanceof InvalidConfigError) {
|
||||
this.config.messages = err.errors;
|
||||
this.ready.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadCliConfig(
|
||||
initializeIfAbsent = true
|
||||
): Promise<DefaultCliConfig | undefined> {
|
||||
): Promise<DefaultCliConfig> {
|
||||
const cliConfigFileUri = await this.getCliConfigFileUri();
|
||||
const cliConfigPath = FileUri.fsPath(cliConfigFileUri);
|
||||
try {
|
||||
@@ -157,7 +198,7 @@ export class ConfigServiceImpl
|
||||
}
|
||||
}
|
||||
|
||||
protected async getFallbackCliConfig(): Promise<DefaultCliConfig> {
|
||||
private async getFallbackCliConfig(): Promise<DefaultCliConfig> {
|
||||
const cliPath = await this.daemon.getExecPath();
|
||||
const rawJson = await spawnCommand(`"${cliPath}"`, [
|
||||
'config',
|
||||
@@ -168,7 +209,7 @@ export class ConfigServiceImpl
|
||||
return JSON.parse(rawJson);
|
||||
}
|
||||
|
||||
protected async initCliConfigTo(fsPathToDir: string): Promise<void> {
|
||||
private async initCliConfigTo(fsPathToDir: string): Promise<void> {
|
||||
const cliPath = await this.daemon.getExecPath();
|
||||
await spawnCommand(`"${cliPath}"`, [
|
||||
'config',
|
||||
@@ -178,7 +219,7 @@ export class ConfigServiceImpl
|
||||
]);
|
||||
}
|
||||
|
||||
protected async mapCliConfigToAppConfig(
|
||||
private async mapCliConfigToAppConfig(
|
||||
cliConfig: DefaultCliConfig
|
||||
): Promise<Config> {
|
||||
const { directories, locale = 'en' } = cliConfig;
|
||||
@@ -199,16 +240,45 @@ export class ConfigServiceImpl
|
||||
};
|
||||
}
|
||||
|
||||
protected fireConfigChanged(config: Config): void {
|
||||
this.configChangeEmitter.fire(config);
|
||||
this.notificationService.notifyConfigDidChange({ config });
|
||||
private fireConfigChanged(
|
||||
oldState: ConfigState,
|
||||
newState: ConfigState
|
||||
): void {
|
||||
this.configChangeEmitter.fire({ oldState, newState });
|
||||
this.notificationService.notifyConfigDidChange(newState);
|
||||
}
|
||||
|
||||
protected fireInvalidConfig(): void {
|
||||
this.notificationService.notifyConfigDidChange({ config: undefined });
|
||||
private async validateCliConfig(config: DefaultCliConfig): Promise<void> {
|
||||
const errors: string[] = [];
|
||||
errors.push(...(await this.checkAccessible(config)));
|
||||
if (errors.length) {
|
||||
throw new InvalidConfigError(errors);
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateDaemon(
|
||||
private async checkAccessible({
|
||||
directories,
|
||||
}: DefaultCliConfig): Promise<string[]> {
|
||||
try {
|
||||
await fs.readdir(directories.user);
|
||||
return [];
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Check accessible failed for input: ${directories.user}`,
|
||||
err
|
||||
);
|
||||
return [
|
||||
nls.localize(
|
||||
'arduino/configuration/cli/inaccessibleDirectory',
|
||||
"Could not access the sketchbook location at '{0}': {1}",
|
||||
directories.user,
|
||||
String(err)
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private async updateDaemon(
|
||||
port: string | number,
|
||||
config: DefaultCliConfig
|
||||
): Promise<void> {
|
||||
@@ -216,7 +286,7 @@ export class ConfigServiceImpl
|
||||
const req = new MergeRequest();
|
||||
const json = JSON.stringify(config, null, 2);
|
||||
req.setJsonData(json);
|
||||
console.log(`Updating daemon with 'data': ${json}`);
|
||||
this.logger.info(`Updating daemon with 'data': ${json}`);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client.merge(req, (error) => {
|
||||
try {
|
||||
@@ -232,7 +302,7 @@ export class ConfigServiceImpl
|
||||
});
|
||||
}
|
||||
|
||||
protected async writeDaemonState(port: string | number): Promise<void> {
|
||||
private async writeDaemonState(port: string | number): Promise<void> {
|
||||
const client = this.createClient(port);
|
||||
const req = new WriteRequest();
|
||||
const cliConfigUri = await this.getCliConfigFileUri();
|
||||
@@ -273,3 +343,13 @@ export class ConfigServiceImpl
|
||||
await fs.mkdir(cliConfig.directories.user, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidConfigError extends Error {
|
||||
constructor(readonly errors: string[]) {
|
||||
super('InvalidConfigError:\n - ' + errors.join('\n - '));
|
||||
if (!errors.length) {
|
||||
throw new Error("Illegal argument: 'messages'. It must not be empty.");
|
||||
}
|
||||
Object.setPrototypeOf(this, InvalidConfigError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user