From d648159f43e32300b228e8b0b89fec340a3b4954 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Tue, 2 Mar 2021 09:48:52 +0100 Subject: [PATCH] ATL-972: Moved the './theia/launch.json' config into a temp folder. Signed-off-by: Akos Kitta --- .../browser/arduino-ide-frontend-module.ts | 5 + .../src/browser/contributions/debug.ts | 9 +- .../debug/debug-configuration-manager.ts | 113 ++++++++++++++++++ .../theia/debug/debug-configuration-model.ts | 50 ++++++++ .../src/common/protocol/sketches-service.ts | 6 + .../src/node/sketches-service-impl.ts | 35 ++---- 6 files changed, 193 insertions(+), 25 deletions(-) create mode 100644 arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts create mode 100644 arduino-ide-extension/src/browser/theia/debug/debug-configuration-model.ts diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 7c4948e0..d7216ac6 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -145,6 +145,8 @@ import { OutputToolbarContribution } from './theia/output/output-toolbar-contrib import { AddZipLibrary } from './contributions/add-zip-library'; import { WorkspaceVariableContribution as TheiaWorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution'; import { WorkspaceVariableContribution } from './theia/workspace/workspace-variable-contribution'; +import { DebugConfigurationManager } from './theia/debug/debug-configuration-manager'; +import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -394,6 +396,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // To remove the `Run` menu item from the application menu. bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope(); rebind(TheiaDebugFrontendApplicationContribution).toService(DebugFrontendApplicationContribution); + // To be able to use a `launch.json` from outside of the workspace. + bind(DebugConfigurationManager).toSelf().inSingletonScope(); + rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager); // Preferences bindArduinoPreferences(bind); diff --git a/arduino-ide-extension/src/browser/contributions/debug.ts b/arduino-ide-extension/src/browser/contributions/debug.ts index e6855d31..193922c4 100644 --- a/arduino-ide-extension/src/browser/contributions/debug.ts +++ b/arduino-ide-extension/src/browser/contributions/debug.ts @@ -106,9 +106,11 @@ export class Debug extends SketchContribution { if (!sketch) { return; } - const [cliPath, sketchPath] = await Promise.all([ + const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(sketch); + const [cliPath, sketchPath, configPath] = await Promise.all([ this.fileService.fsPath(new URI(executables.cliUri)), - this.fileService.fsPath(new URI(sketch.uri)) + this.fileService.fsPath(new URI(sketch.uri)), + this.fileService.fsPath(new URI(ideTempFolderUri)), ]) const config = { cliPath, @@ -116,7 +118,8 @@ export class Debug extends SketchContribution { fqbn, name }, - sketchPath + sketchPath, + configPath }; return this.commandService.executeCommand('arduino.debug.start', config); } diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts b/arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts new file mode 100644 index 00000000..5598ba26 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts @@ -0,0 +1,113 @@ +import debounce = require('p-debounce'); +import { inject, injectable, postConstruct } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { Event, Emitter } from '@theia/core/lib/common/event'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { DebugConfiguration } from '@theia/debug/lib/common/debug-common'; +import { DebugConfigurationModel as TheiaDebugConfigurationModel } from '@theia/debug/lib/browser/debug-configuration-model'; +import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager'; +import { SketchesService } from '../../../common/protocol'; +import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl'; +import { DebugConfigurationModel } from './debug-configuration-model'; +import { FileOperationError, FileOperationResult } from '@theia/filesystem/lib/common/files'; + +@injectable() +export class DebugConfigurationManager extends TheiaDebugConfigurationManager { + + @inject(SketchesService) + protected readonly sketchesService: SketchesService; + + @inject(SketchesServiceClientImpl) + protected readonly sketchesServiceClient: SketchesServiceClientImpl; + + @inject(FrontendApplicationStateService) + protected readonly appStateService: FrontendApplicationStateService; + + protected onTempContentDidChangeEmitter = new Emitter(); + get onTempContentDidChange(): Event { + return this.onTempContentDidChangeEmitter.event; + } + + @postConstruct() + protected async init(): Promise { + super.init(); + this.appStateService.reachedState('ready').then(async () => { + const tempContent = await this.getTempLaunchJsonContent(); + if (!tempContent) { + // No active sketch. + return; + } + // Watch the file of the container folder. + this.fileService.watch(tempContent instanceof URI ? tempContent : tempContent.uri); + // Use the normalized temp folder name. We cannot compare Theia URIs here. + // /var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T/arduino-ide2-a0337d47f86b24a51df3dbcf2cc17925/launch.json + // /private/var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T/arduino-ide2-A0337D47F86B24A51DF3DBCF2CC17925/launch.json + const tempFolderName = (tempContent instanceof URI ? tempContent : tempContent.uri.parent).path.base.toLowerCase(); + this.fileService.onDidFilesChange(event => { + for (const { resource } of event.changes) { + if (resource.path.base === 'launch.json' && resource.parent.path.base.toLowerCase() === tempFolderName) { + this.getTempLaunchJsonContent().then(config => { + if (config && !(config instanceof URI)) { + this.onTempContentDidChangeEmitter.fire(config); + } + }); + break; + } + } + }); + this.updateModels(); + }); + } + + protected updateModels = debounce(async () => { + await this.appStateService.reachedState('ready'); + const roots = await this.workspaceService.roots; + const toDelete = new Set(this.models.keys()); + for (const rootStat of roots) { + const key = rootStat.resource.toString(); + toDelete.delete(key); + if (!this.models.has(key)) { + const tempContent = await this.getTempLaunchJsonContent(); + if (!tempContent) { + continue; + } + const configurations: DebugConfiguration[] = tempContent instanceof URI ? [] : tempContent.configurations; + const uri = tempContent instanceof URI ? undefined : tempContent.uri; + const model = new DebugConfigurationModel(key, this.preferences, configurations, uri, this.onTempContentDidChange); + model.onDidChange(() => this.updateCurrent()); + model.onDispose(() => this.models.delete(key)); + this.models.set(key, model); + } + } + for (const uri of toDelete) { + const model = this.models.get(uri); + if (model) { + model.dispose(); + } + } + this.updateCurrent(); + }, 500); + + protected async getTempLaunchJsonContent(): Promise { + const sketch = await this.sketchesServiceClient.currentSketch(); + if (!sketch) { + return undefined; + } + const uri = await this.sketchesService.getIdeTempFolderUri(sketch); + const tempFolderUri = new URI(uri); + await this.fileService.createFolder(tempFolderUri); + try { + const uri = tempFolderUri.resolve('launch.json'); + const { value } = await this.fileService.read(uri); + const configurations = DebugConfigurationModel.parse(JSON.parse(value)); + return { uri, configurations }; + } catch (err) { + if (err instanceof FileOperationError && err.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return tempFolderUri; + } + console.error('Could not load debug configuration from IDE2 temp folder.', err); + throw err; + } + } + +} diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-configuration-model.ts b/arduino-ide-extension/src/browser/theia/debug/debug-configuration-model.ts new file mode 100644 index 00000000..711e7989 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/debug/debug-configuration-model.ts @@ -0,0 +1,50 @@ +import { Event } from '@theia/core/lib/common/event'; +import URI from '@theia/core/lib/common/uri'; +import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service'; +import { DebugConfiguration } from '@theia/debug/lib/common/debug-common'; +import { DebugConfigurationModel as TheiaDebugConfigurationModel } from '@theia/debug/lib/browser/debug-configuration-model'; + +export class DebugConfigurationModel extends TheiaDebugConfigurationModel { + + constructor( + readonly workspaceFolderUri: string, + protected readonly preferences: PreferenceService, + protected readonly config: DebugConfiguration[], + protected configUri: URI | undefined, + protected readonly onConfigDidChange: Event) { + + super(workspaceFolderUri, preferences); + this.toDispose.push(onConfigDidChange(content => { + const { uri, configurations } = content; + this.configUri = uri; + this.config.length = 0; + this.config.push(...configurations); + this.reconcile(); + })); + this.reconcile(); + } + + protected parseConfigurations(): TheiaDebugConfigurationModel.JsonContent { + return { + uri: this.configUri, + configurations: this.config + }; + } + +} + +export namespace DebugConfigurationModel { + export function parse(launchConfig: any): DebugConfiguration[] { + const configurations: DebugConfiguration[] = []; + if (launchConfig && typeof launchConfig === 'object' && 'configurations' in launchConfig) { + if (Array.isArray(launchConfig.configurations)) { + for (const configuration of launchConfig.configurations) { + if (DebugConfiguration.is(configuration)) { + configurations.push(configuration); + } + } + } + } + return configurations; + } +} diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index cf619800..4855a02a 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -63,6 +63,12 @@ export interface SketchesService { */ archive(sketch: Sketch, destinationUri: string): Promise; + /** + * Counterpart of the CLI's `genBuildPath` functionality. + * Based on https://github.com/arduino/arduino-cli/blob/550179eefd2d2bca299d50a4af9e9bfcfebec649/arduino/builder/builder.go#L30-L38 + */ + getIdeTempFolderUri(sketch: Sketch): Promise; + } export interface Sketch { diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index 48354cc2..a2d6321a 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as temp from 'temp'; import * as path from 'path'; +import * as crypto from 'crypto'; import { ncp } from 'ncp'; import { promisify } from 'util'; import URI from '@theia/core/lib/common/uri'; @@ -13,7 +14,7 @@ import { SketchesService, Sketch } from '../common/protocol/sketches-service'; import { firstToLowerCase } from '../common/utils'; import { NotificationServiceServerImpl } from './notification-service-server'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; -import { CoreClientProvider } from './core-client-provider'; +import { CoreClientAware } from './core-client-provider'; import { LoadSketchReq, ArchiveSketchReq } from './cli-protocol/commands/commands_pb'; const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/; @@ -21,14 +22,11 @@ const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/; const prefix = '.arduinoIDE-unsaved'; @injectable() -export class SketchesServiceImpl implements SketchesService { +export class SketchesServiceImpl extends CoreClientAware implements SketchesService { @inject(ConfigService) protected readonly configService: ConfigService; - @inject(CoreClientProvider) - protected readonly coreClientProvider: CoreClientProvider; - @inject(NotificationServiceServerImpl) protected readonly notificationService: NotificationServiceServerImpl; @@ -348,23 +346,16 @@ void loop() { return destinationUri; } - private async coreClient(): Promise { - const coreClient = await new Promise(async resolve => { - const client = await this.coreClientProvider.client(); - if (client) { - resolve(client); - return; - } - const toDispose = this.coreClientProvider.onClientReady(async () => { - const client = await this.coreClientProvider.client(); - if (client) { - toDispose.dispose(); - resolve(client); - return; - } - }); - }); - return coreClient; + async getIdeTempFolderUri(sketch: Sketch): Promise { + const genBuildPath = await this.getIdeTempFolderPath(sketch); + return FileUri.create(genBuildPath).toString(); + } + + async getIdeTempFolderPath(sketch: Sketch): Promise { + const sketchPath = FileUri.fsPath(sketch.uri); + await fs.promises.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible. + const suffix = crypto.createHash('md5').update(sketchPath).digest('hex'); + return path.join(os.tmpdir(), `arduino-ide2-${suffix}`); } }