diff --git a/CHANGELOG.md b/CHANGELOG.md index ecd2d342c9cfb..3f821fc0e16a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - `visual`: Display a visual preview of the tab. (The preview support was added with this PR) - [repo] updated GitHub workflow to stop publishing `next` versions [#12699](https://github.com/eclipse-theia/theia/pull/12699) - [workspace] split `CommonWorkspaceUtils` into `WorkspaceFileService` and `UntitledWorkspaceService` [#12420](https://github.com/eclipse-theia/theia/pull/12420) +- [plugin] Removed synchronous `fs` calls from the backend application and plugins. The plugin scanner, directory and file handlers, and the plugin deploy entry has async API now. Internal `protected` APIs have been affected. [#12798](https://github.com/eclipse-theia/theia/pull/12798) ## v1.39.0 - 06/29/2023 diff --git a/packages/core/src/common/promise-util.spec.ts b/packages/core/src/common/promise-util.spec.ts index 6324c86da2be6..051cbd4fa78c7 100644 --- a/packages/core/src/common/promise-util.spec.ts +++ b/packages/core/src/common/promise-util.spec.ts @@ -13,22 +13,19 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import * as assert from 'assert'; -import { waitForEvent } from './promise-util'; +import * as assert from 'assert/strict'; +import { firstTrue, waitForEvent } from './promise-util'; import { Emitter } from './event'; +import { CancellationError } from './cancellation'; describe('promise-util', () => { - it('should time out', async () => { - const emitter = new Emitter(); - try { - await waitForEvent(emitter.event, 1000); - assert.fail('did not time out'); - } catch (e) { - // OK - } - }); - describe('promise-util', () => { + describe('waitForEvent', () => { + it('should time out', async () => { + const emitter = new Emitter(); + await assert.rejects(waitForEvent(emitter.event, 1000), reason => reason instanceof CancellationError); + }); + it('should get event', async () => { const emitter = new Emitter(); setTimeout(() => { @@ -38,4 +35,38 @@ describe('promise-util', () => { }); }); + describe('firstTrue', () => { + it('should resolve to false when the promises arg is empty', async () => { + const actual = await firstTrue(); + assert.strictEqual(actual, false); + }); + + it('should resolve to true when the first promise resolves to true', async () => { + const signals: string[] = []; + const createPromise = (signal: string, timeout: number, result: boolean) => + new Promise(resolve => setTimeout(() => { + signals.push(signal); + resolve(result); + }, timeout)); + const actual = await firstTrue( + createPromise('a', 10, false), + createPromise('b', 20, false), + createPromise('c', 30, true), + createPromise('d', 40, false), + createPromise('e', 50, true) + ); + assert.strictEqual(actual, true); + assert.deepStrictEqual(signals, ['a', 'b', 'c']); + }); + + it('should reject when one of the promises rejects', async () => { + await assert.rejects(firstTrue( + new Promise(resolve => setTimeout(() => resolve(false), 10)), + new Promise(resolve => setTimeout(() => resolve(false), 20)), + new Promise((_, reject) => setTimeout(() => reject(new Error('my test error')), 30)), + new Promise(resolve => setTimeout(() => resolve(true), 40)), + ), /Error: my test error/); + }); + }); + }); diff --git a/packages/core/src/common/promise-util.ts b/packages/core/src/common/promise-util.ts index f54362e13c8e5..841eb0af43a27 100644 --- a/packages/core/src/common/promise-util.ts +++ b/packages/core/src/common/promise-util.ts @@ -129,3 +129,15 @@ export function waitForEvent(event: Event, ms: number, thisArg?: any, disp export function isThenable(obj: unknown): obj is Promise { return isObject>(obj) && isFunction(obj.then); } + +/** + * Returns with a promise that waits until the first promise resolves to `true`. + */ +// Based on https://stackoverflow.com/a/51160727/5529090 +export function firstTrue(...promises: readonly Promise[]): Promise { + const newPromises = promises.map(promise => new Promise( + (resolve, reject) => promise.then(result => result && resolve(true), reject) + )); + newPromises.push(Promise.all(promises).then(() => false)); + return Promise.race(newPromises); +} diff --git a/packages/git/src/node/dugite-git.ts b/packages/git/src/node/dugite-git.ts index c499659bbb24c..e72ad9dee1724 100644 --- a/packages/git/src/node/dugite-git.ts +++ b/packages/git/src/node/dugite-git.ts @@ -766,7 +766,8 @@ export class DugiteGit implements Git { const out = result.stdout; if (out && out.length !== 0) { try { - return fs.realpathSync(out.trim()); + const realpath = await fs.realpath(out.trim()); + return realpath; } catch (e) { this.logger.error(e); return undefined; diff --git a/packages/git/src/node/git-locator/git-locator-impl.ts b/packages/git/src/node/git-locator/git-locator-impl.ts index 7e84af071a1cb..8c6c8d69d9f3f 100644 --- a/packages/git/src/node/git-locator/git-locator-impl.ts +++ b/packages/git/src/node/git-locator/git-locator-impl.ts @@ -59,7 +59,7 @@ export class GitLocatorImpl implements GitLocator { } protected async doLocate(basePath: string, context: GitLocateContext): Promise { - const realBasePath = fs.realpathSync(basePath); + const realBasePath = await fs.realpath(basePath); if (context.visited.has(realBasePath)) { return []; } @@ -77,9 +77,9 @@ export class GitLocatorImpl implements GitLocator { } }); if (context.maxCount >= 0 && paths.length >= context.maxCount) { - return paths.slice(0, context.maxCount).map(GitLocatorImpl.map); + return await Promise.all(paths.slice(0, context.maxCount).map(GitLocatorImpl.map)); } - const repositoryPaths = paths.map(GitLocatorImpl.map); + const repositoryPaths = await Promise.all(paths.map(GitLocatorImpl.map)); return this.locateFrom( newContext => this.generateNested(repositoryPaths, newContext), context, @@ -145,8 +145,8 @@ export class GitLocatorImpl implements GitLocator { return result; } - static map(repository: string): string { - return fs.realpathSync(path.dirname(repository)); + static async map(repository: string): Promise { + return fs.realpath(path.dirname(repository)); } } diff --git a/packages/plugin-dev/src/node/hosted-instance-manager.ts b/packages/plugin-dev/src/node/hosted-instance-manager.ts index a0f45a66cb02c..dc11d18603478 100644 --- a/packages/plugin-dev/src/node/hosted-instance-manager.ts +++ b/packages/plugin-dev/src/node/hosted-instance-manager.ts @@ -30,6 +30,7 @@ import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/node/hosted-pl import { MetadataScanner } from '@theia/plugin-ext/lib/hosted/node/metadata-scanner'; import { PluginDebugConfiguration } from '../common/plugin-dev-protocol'; import { HostedPluginProcess } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin-process'; +import { isENOENT } from '@theia/plugin-ext/lib/common/errors'; const DEFAULT_HOSTED_PLUGIN_PORT = 3030; @@ -84,7 +85,7 @@ export interface HostedInstanceManager { * * @param uri uri to the plugin source location */ - isPluginValid(uri: URI): boolean; + isPluginValid(uri: URI): Promise; } const HOSTED_INSTANCE_START_TIMEOUT_MS = 30000; @@ -224,19 +225,18 @@ export abstract class AbstractHostedInstanceManager implements HostedInstanceMan } } - isPluginValid(uri: URI): boolean { + async isPluginValid(uri: URI): Promise { const pckPath = path.join(FileUri.fsPath(uri), 'package.json'); - if (fs.existsSync(pckPath)) { - const pck = fs.readJSONSync(pckPath); - try { - this.metadata.getScanner(pck); - return true; - } catch (e) { - console.error(e); - return false; + try { + const pck = await fs.readJSON(pckPath); + this.metadata.getScanner(pck); + return true; + } catch (err) { + if (!isENOENT(err)) { + console.error(err); } + return false; } - return false; } protected async getStartCommand(port?: number, debugConfig?: PluginDebugConfiguration): Promise { diff --git a/packages/plugin-dev/src/node/hosted-plugins-manager.ts b/packages/plugin-dev/src/node/hosted-plugins-manager.ts index db9d3dede9e08..c6eadb024a48e 100644 --- a/packages/plugin-dev/src/node/hosted-plugins-manager.ts +++ b/packages/plugin-dev/src/node/hosted-plugins-manager.ts @@ -131,15 +131,15 @@ export class HostedPluginsManagerImpl implements HostedPluginsManager { * * @param pluginPath path to plugin's root directory */ - protected checkWatchScript(pluginPath: string): boolean { + protected async checkWatchScript(pluginPath: string): Promise { const pluginPackageJsonPath = path.join(pluginPath, 'package.json'); - if (fs.existsSync(pluginPackageJsonPath)) { - const packageJson = fs.readJSONSync(pluginPackageJsonPath); + try { + const packageJson = await fs.readJSON(pluginPackageJsonPath); const scripts = packageJson['scripts']; if (scripts && scripts['watch']) { return true; } - } + } catch { } return false; } diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts index 3dfd2c1230143..f135aead0413e 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts @@ -18,33 +18,47 @@ import * as path from 'path'; import * as filenamify from 'filenamify'; import * as fs from '@theia/core/shared/fs-extra'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { RecursivePartial } from '@theia/core'; +import type { RecursivePartial, URI } from '@theia/core'; +import { Deferred, firstTrue } from '@theia/core/lib/common/promise-util'; +import { getTempDirPathAsync } from '@theia/plugin-ext/lib/main/node/temp-dir-util'; import { PluginDeployerDirectoryHandler, PluginDeployerEntry, PluginDeployerDirectoryHandlerContext, PluginDeployerEntryType, PluginPackage, PluginType, PluginIdentifiers } from '@theia/plugin-ext'; import { FileUri } from '@theia/core/lib/node'; -import { getTempDir } from '@theia/plugin-ext/lib/main/node/temp-dir-util'; import { PluginCliContribution } from '@theia/plugin-ext/lib/main/node/plugin-cli-contribution'; @injectable() export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHandler { - protected readonly deploymentDirectory = FileUri.create(getTempDir('vscode-copied')); + protected readonly deploymentDirectory: Deferred; @inject(PluginCliContribution) protected readonly pluginCli: PluginCliContribution; - accept(plugin: PluginDeployerEntry): boolean { + constructor() { + this.deploymentDirectory = new Deferred(); + getTempDirPathAsync('vscode-copied') + .then(deploymentDirectoryPath => this.deploymentDirectory.resolve(FileUri.create(deploymentDirectoryPath))); + } + + async accept(plugin: PluginDeployerEntry): Promise { console.debug(`Resolving "${plugin.id()}" as a VS Code extension...`); return this.attemptResolution(plugin); } - protected attemptResolution(plugin: PluginDeployerEntry): boolean { - return this.resolvePackage(plugin) || this.deriveMetadata(plugin); + protected async attemptResolution(plugin: PluginDeployerEntry): Promise { + if (this.resolvePackage(plugin)) { + return true; + } + return this.deriveMetadata(plugin); } - protected deriveMetadata(plugin: PluginDeployerEntry): boolean { - return this.resolveFromSources(plugin) || this.resolveFromVSIX(plugin) || this.resolveFromNpmTarball(plugin); + protected async deriveMetadata(plugin: PluginDeployerEntry): Promise { + return firstTrue( + this.resolveFromSources(plugin), + this.resolveFromVSIX(plugin), + this.resolveFromNpmTarball(plugin) + ); } async handle(context: PluginDeployerDirectoryHandlerContext): Promise { @@ -68,11 +82,12 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand const origin = entry.originalPath(); const targetDir = await this.getExtensionDir(context); try { - if (fs.existsSync(targetDir) || !entry.path().startsWith(origin)) { + if (await fs.pathExists(targetDir) || !entry.path().startsWith(origin)) { console.log(`[${id}]: already copied.`); } else { console.log(`[${id}]: copying to "${targetDir}"`); - await fs.mkdirp(FileUri.fsPath(this.deploymentDirectory)); + const deploymentDirectory = await this.deploymentDirectory.promise; + await fs.mkdirp(FileUri.fsPath(deploymentDirectory)); await context.copy(origin, targetDir); entry.updatePath(targetDir); if (!this.deriveMetadata(entry)) { @@ -86,22 +101,25 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand } } - protected resolveFromSources(plugin: PluginDeployerEntry): boolean { + protected async resolveFromSources(plugin: PluginDeployerEntry): Promise { const pluginPath = plugin.path(); - return this.resolvePackage(plugin, { pluginPath, pck: this.requirePackage(pluginPath) }); + const pck = await this.requirePackage(pluginPath); + return this.resolvePackage(plugin, { pluginPath, pck }); } - protected resolveFromVSIX(plugin: PluginDeployerEntry): boolean { - if (!fs.existsSync(path.join(plugin.path(), 'extension.vsixmanifest'))) { + protected async resolveFromVSIX(plugin: PluginDeployerEntry): Promise { + if (!(await fs.pathExists(path.join(plugin.path(), 'extension.vsixmanifest')))) { return false; } const pluginPath = path.join(plugin.path(), 'extension'); - return this.resolvePackage(plugin, { pluginPath, pck: this.requirePackage(pluginPath) }); + const pck = await this.requirePackage(pluginPath); + return this.resolvePackage(plugin, { pluginPath, pck }); } - protected resolveFromNpmTarball(plugin: PluginDeployerEntry): boolean { + protected async resolveFromNpmTarball(plugin: PluginDeployerEntry): Promise { const pluginPath = path.join(plugin.path(), 'package'); - return this.resolvePackage(plugin, { pluginPath, pck: this.requirePackage(pluginPath) }); + const pck = await this.requirePackage(pluginPath); + return this.resolvePackage(plugin, { pluginPath, pck }); } protected resolvePackage(plugin: PluginDeployerEntry, options?: { @@ -125,9 +143,9 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand return true; } - protected requirePackage(pluginPath: string): PluginPackage | undefined { + protected async requirePackage(pluginPath: string): Promise { try { - const plugin = fs.readJSONSync(path.join(pluginPath, 'package.json')) as PluginPackage; + const plugin: PluginPackage = await fs.readJSON(path.join(pluginPath, 'package.json')); plugin.publisher ??= PluginIdentifiers.UNPUBLISHED; return plugin; } catch { @@ -136,6 +154,7 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand } protected async getExtensionDir(context: PluginDeployerDirectoryHandlerContext): Promise { - return FileUri.fsPath(this.deploymentDirectory.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' }))); + const deploymentDirectory = await this.deploymentDirectory.promise; + return FileUri.fsPath(deploymentDirectory.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' }))); } } diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts index 1786958a9373e..23e3e3e9292d3 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts @@ -18,8 +18,10 @@ import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandl import * as fs from '@theia/core/shared/fs-extra'; import * as path from 'path'; import * as filenamify from 'filenamify'; -import { injectable, inject } from '@theia/core/shared/inversify'; -import { getTempDir } from '@theia/plugin-ext/lib/main/node/temp-dir-util'; +import type { URI } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { getTempDirPathAsync } from '@theia/plugin-ext/lib/main/node/temp-dir-util'; import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment'; import { FileUri } from '@theia/core/lib/node/file-uri'; @@ -31,13 +33,21 @@ export class PluginVsCodeFileHandler implements PluginDeployerFileHandler { @inject(PluginVSCodeEnvironment) protected readonly environment: PluginVSCodeEnvironment; - private readonly systemExtensionsDirUri = FileUri.create(getTempDir('vscode-unpacked')); + private readonly systemExtensionsDirUri: Deferred; - accept(resolvedPlugin: PluginDeployerEntry): boolean { - if (!resolvedPlugin.isFile()) { - return false; - } - return isVSCodePluginFile(resolvedPlugin.path()); + constructor() { + this.systemExtensionsDirUri = new Deferred(); + getTempDirPathAsync('vscode-unpacked') + .then(systemExtensionsDirPath => this.systemExtensionsDirUri.resolve(FileUri.create(systemExtensionsDirPath))); + } + + async accept(resolvedPlugin: PluginDeployerEntry): Promise { + return resolvedPlugin.isFile().then(file => { + if (!file) { + return false; + } + return isVSCodePluginFile(resolvedPlugin.path()); + }); } async handle(context: PluginDeployerFileHandlerContext): Promise { @@ -55,7 +65,8 @@ export class PluginVsCodeFileHandler implements PluginDeployerFileHandler { } protected async getExtensionDir(context: PluginDeployerFileHandlerContext): Promise { - return FileUri.fsPath(this.systemExtensionsDirUri.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' }))); + const systemExtensionsDirUri = await this.systemExtensionsDirUri.promise; + return FileUri.fsPath(systemExtensionsDirUri.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' }))); } protected async decompress(extensionDir: string, context: PluginDeployerFileHandlerContext): Promise { diff --git a/packages/plugin-ext/src/common/errors.ts b/packages/plugin-ext/src/common/errors.ts index 37b3a31560147..b81c09f4b312c 100644 --- a/packages/plugin-ext/src/common/errors.ts +++ b/packages/plugin-ext/src/common/errors.ts @@ -14,6 +14,8 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import { isObject } from '@theia/core/lib/common/types'; + export function illegalArgument(message?: string): Error { if (message) { return new Error(`Illegal argument: ${message}`); @@ -35,3 +37,27 @@ export function disposed(what: string): Error { result.name = 'DISPOSED'; return result; } + +interface Errno { + readonly code: string; + readonly errno: number +} +const ENOENT = 'ENOENT' as const; + +type ErrnoException = Error & Errno; +function isErrnoException(arg: unknown): arg is ErrnoException { + return arg instanceof Error + && isObject>(arg) + && typeof arg.code === 'string' + && typeof arg.errno === 'number'; +} + +/** + * _(No such file or directory)_: Commonly raised by `fs` operations to indicate that a component of the specified pathname does not exist — no entity (file or directory) could be + * found by the given path. + */ +export function isENOENT( + arg: unknown +): arg is ErrnoException & Readonly<{ code: typeof ENOENT }> { + return isErrnoException(arg) && arg.code === ENOENT; +} diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 32f27ae6178ba..ca1900bc8a6f7 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -348,7 +348,7 @@ export interface PluginScanner { */ getLifecycle(plugin: PluginPackage): PluginLifecycle; - getContribution(plugin: PluginPackage): PluginContribution | undefined; + getContribution(plugin: PluginPackage): Promise; /** * A mapping between a dependency as its defined in package.json @@ -376,7 +376,7 @@ export interface PluginDeployerResolver { export const PluginDeployerDirectoryHandler = Symbol('PluginDeployerDirectoryHandler'); export interface PluginDeployerDirectoryHandler { - accept(pluginDeployerEntry: PluginDeployerEntry): boolean; + accept(pluginDeployerEntry: PluginDeployerEntry): Promise; handle(context: PluginDeployerDirectoryHandlerContext): Promise; } @@ -384,7 +384,7 @@ export interface PluginDeployerDirectoryHandler { export const PluginDeployerFileHandler = Symbol('PluginDeployerFileHandler'); export interface PluginDeployerFileHandler { - accept(pluginDeployerEntry: PluginDeployerEntry): boolean; + accept(pluginDeployerEntry: PluginDeployerEntry): Promise; handle(context: PluginDeployerFileHandlerContext): Promise; } @@ -477,9 +477,9 @@ export interface PluginDeployerEntry { getChanges(): string[]; - isFile(): boolean; + isFile(): Promise; - isDirectory(): boolean; + isDirectory(): Promise; /** * Resolved if a resolver has handle this plugin diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts index 218fa1f072881..f351afc73c772 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts +++ b/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts @@ -173,7 +173,7 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler { const { type } = entry; const deployed: DeployedPlugin = { metadata, type }; - deployed.contributes = this.reader.readContribution(manifest); + deployed.contributes = await this.reader.readContribution(manifest); await this.localizationService.deployLocalizations(deployed); deployedPlugins.set(id, deployed); deployPlugin.debug(`Deployed ${entryPoint} plugin "${id}" from "${metadata.model.entryPoint[entryPoint] || pluginPath}"`); diff --git a/packages/plugin-ext/src/hosted/node/plugin-reader.ts b/packages/plugin-ext/src/hosted/node/plugin-reader.ts index 0520e887faadc..a9ba83ef0afdb 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-reader.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-reader.ts @@ -118,7 +118,7 @@ export class HostedPluginReader implements BackendApplicationContribution { return pluginMetadata; } - readContribution(plugin: PluginPackage): PluginContribution | undefined { + async readContribution(plugin: PluginPackage): Promise { const scanner = this.scanner.getScanner(plugin); return scanner.getContribution(plugin); } diff --git a/packages/plugin-ext/src/hosted/node/scanners/grammars-reader.ts b/packages/plugin-ext/src/hosted/node/scanners/grammars-reader.ts index e021ac03181cc..48a47b8479f52 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/grammars-reader.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/grammars-reader.ts @@ -22,10 +22,10 @@ import * as fs from '@theia/core/shared/fs-extra'; @injectable() export class GrammarsReader { - readGrammars(rawGrammars: PluginPackageGrammarsContribution[], pluginPath: string): GrammarsContribution[] { + async readGrammars(rawGrammars: PluginPackageGrammarsContribution[], pluginPath: string): Promise { const result = new Array(); for (const rawGrammar of rawGrammars) { - const grammar = this.readGrammar(rawGrammar, pluginPath); + const grammar = await this.readGrammar(rawGrammar, pluginPath); if (grammar) { result.push(grammar); } @@ -34,13 +34,14 @@ export class GrammarsReader { return result; } - private readGrammar(rawGrammar: PluginPackageGrammarsContribution, pluginPath: string): GrammarsContribution | undefined { + private async readGrammar(rawGrammar: PluginPackageGrammarsContribution, pluginPath: string): Promise { // TODO: validate inputs let grammar: string | object; + if (rawGrammar.path.endsWith('json')) { - grammar = fs.readJSONSync(path.resolve(pluginPath, rawGrammar.path)); + grammar = await fs.readJSON(path.resolve(pluginPath, rawGrammar.path)); } else { - grammar = fs.readFileSync(path.resolve(pluginPath, rawGrammar.path), 'utf8'); + grammar = await fs.readFile(path.resolve(pluginPath, rawGrammar.path), 'utf8'); } return { language: rawGrammar.language, diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index 28306abb2af04..d1b2c24f29115 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -62,11 +62,12 @@ import { PluginIdentifiers, TerminalProfile } from '../../../common/plugin-protocol'; -import * as fs from 'fs'; +import { promises as fs } from 'fs'; import * as path from 'path'; import { isObject, isStringArray, RecursivePartial } from '@theia/core/lib/common/types'; import { GrammarsReader } from './grammars-reader'; import { CharacterPair } from '../../../common/plugin-api-rpc'; +import { isENOENT } from '../../../common/errors'; import * as jsoncparser from 'jsonc-parser'; import { IJSONSchema } from '@theia/core/lib/common/json-schema'; import { deepClone } from '@theia/core/lib/common/objects'; @@ -144,7 +145,7 @@ export class TheiaPluginScanner implements PluginScanner { return undefined; } - getContribution(rawPlugin: PluginPackage): PluginContribution | undefined { + async getContribution(rawPlugin: PluginPackage): Promise { if (!rawPlugin.contributes && !rawPlugin.activationEvents) { return undefined; } @@ -175,15 +176,6 @@ export class TheiaPluginScanner implements PluginScanner { const configurationDefaults = rawPlugin.contributes.configurationDefaults; contributions.configurationDefaults = PreferenceSchemaProperties.is(configurationDefaults) ? configurationDefaults : undefined; - try { - if (rawPlugin.contributes.languages) { - const languages = this.readLanguages(rawPlugin.contributes.languages, rawPlugin.packagePath); - contributions.languages = languages; - } - } catch (err) { - console.error(`Could not read '${rawPlugin.name}' contribution 'languages'.`, rawPlugin.contributes.languages, err); - } - try { if (rawPlugin.contributes.submenus) { contributions.submenus = this.readSubmenus(rawPlugin.contributes.submenus, rawPlugin); @@ -192,15 +184,6 @@ export class TheiaPluginScanner implements PluginScanner { console.error(`Could not read '${rawPlugin.name}' contribution 'submenus'.`, rawPlugin.contributes.submenus, err); } - try { - if (rawPlugin.contributes.grammars) { - const grammars = this.grammarsReader.readGrammars(rawPlugin.contributes.grammars, rawPlugin.packagePath); - contributions.grammars = grammars; - } - } catch (err) { - console.error(`Could not read '${rawPlugin.name}' contribution 'grammars'.`, rawPlugin.contributes.grammars, err); - } - try { if (rawPlugin.contributes.customEditors) { const customEditors = this.readCustomEditors(rawPlugin.contributes.customEditors); @@ -354,18 +337,40 @@ export class TheiaPluginScanner implements PluginScanner { console.error(`Could not read '${rawPlugin.name}' contribution 'colors'.`, rawPlugin.contributes.colors, err); } - try { - contributions.localizations = this.readLocalizations(rawPlugin); - } catch (err) { - console.error(`Could not read '${rawPlugin.name}' contribution 'localizations'.`, rawPlugin.contributes.colors, err); - } - try { contributions.terminalProfiles = this.readTerminals(rawPlugin); } catch (err) { console.error(`Could not read '${rawPlugin.name}' contribution 'terminals'.`, rawPlugin.contributes.terminal, err); } + const [localizationsResult, languagesResult, grammarsResult] = await Promise.allSettled([ + this.readLocalizations(rawPlugin), + rawPlugin.contributes.languages ? this.readLanguages(rawPlugin.contributes.languages, rawPlugin.packagePath) : undefined, + rawPlugin.contributes.grammars ? this.grammarsReader.readGrammars(rawPlugin.contributes.grammars, rawPlugin.packagePath) : undefined + ]); + + if (localizationsResult.status === 'fulfilled') { + contributions.localizations = localizationsResult.value; + } else { + console.error(`Could not read '${rawPlugin.name}' contribution 'localizations'.`, rawPlugin.contributes.localizations, localizationsResult.reason); + } + + if (rawPlugin.contributes.languages) { + if (languagesResult.status === 'fulfilled') { + contributions.languages = languagesResult.value; + } else { + console.error(`Could not read '${rawPlugin.name}' contribution 'languages'.`, rawPlugin.contributes.languages, languagesResult.reason); + } + } + + if (rawPlugin.contributes.grammars) { + if (grammarsResult.status === 'fulfilled') { + contributions.grammars = grammarsResult.value; + } else { + console.error(`Could not read '${rawPlugin.name}' contribution 'grammars'.`, rawPlugin.contributes.grammars, grammarsResult.reason); + } + } + return contributions; } @@ -376,26 +381,26 @@ export class TheiaPluginScanner implements PluginScanner { return pck.contributes.terminal.profiles.filter(profile => profile.id && profile.title); } - protected readLocalizations(pck: PluginPackage): Localization[] | undefined { + protected async readLocalizations(pck: PluginPackage): Promise { if (!pck.contributes || !pck.contributes.localizations) { return undefined; } - return pck.contributes.localizations.map(e => this.readLocalization(e, pck.packagePath)); + return Promise.all(pck.contributes.localizations.map(e => this.readLocalization(e, pck.packagePath))); } - protected readLocalization({ languageId, languageName, localizedLanguageName, translations }: PluginPackageLocalization, pluginPath: string): Localization { + protected async readLocalization({ languageId, languageName, localizedLanguageName, translations }: PluginPackageLocalization, pluginPath: string): Promise { const local: Localization = { languageId, languageName, localizedLanguageName, translations: [] }; - local.translations = translations.map(e => this.readTranslation(e, pluginPath)); + local.translations = await Promise.all(translations.map(e => this.readTranslation(e, pluginPath))); return local; } - protected readTranslation(packageTranslation: PluginPackageTranslation, pluginPath: string): Translation { - const translation = this.readJson(path.resolve(pluginPath, packageTranslation.path)); + protected async readTranslation(packageTranslation: PluginPackageTranslation, pluginPath: string): Promise { + const translation = await this.readJson(path.resolve(pluginPath, packageTranslation.path)); if (!translation) { throw new Error(`Could not read json file '${packageTranslation.path}'.`); } @@ -529,15 +534,18 @@ export class TheiaPluginScanner implements PluginScanner { return result; } - protected readJson(filePath: string): T | undefined { - const content = this.readFileSync(filePath); + protected async readJson(filePath: string): Promise { + const content = await this.readFile(filePath); return content ? jsoncparser.parse(content, undefined, { disallowComments: false }) : undefined; } - protected readFileSync(filePath: string): string { + protected async readFile(filePath: string): Promise { try { - return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : ''; + const content = await fs.readFile(filePath, { encoding: 'utf8' }); + return content; } catch (e) { - console.error(e); + if (!isENOENT(e)) { + console.error(e); + } return ''; } } @@ -644,8 +652,8 @@ export class TheiaPluginScanner implements PluginScanner { return result; } - private readLanguages(rawLanguages: PluginPackageLanguageContribution[], pluginPath: string): LanguageContribution[] { - return rawLanguages.map(language => this.readLanguage(language, pluginPath)); + private async readLanguages(rawLanguages: PluginPackageLanguageContribution[], pluginPath: string): Promise { + return Promise.all(rawLanguages.map(language => this.readLanguage(language, pluginPath))); } private readSubmenus(rawSubmenus: PluginPackageSubmenu[], plugin: PluginPackage): Submenu[] { @@ -662,7 +670,7 @@ export class TheiaPluginScanner implements PluginScanner { } - private readLanguage(rawLang: PluginPackageLanguageContribution, pluginPath: string): LanguageContribution { + private async readLanguage(rawLang: PluginPackageLanguageContribution, pluginPath: string): Promise { // TODO: add validation to all parameters const result: LanguageContribution = { id: rawLang.id, @@ -674,7 +682,7 @@ export class TheiaPluginScanner implements PluginScanner { mimetypes: rawLang.mimetypes }; if (rawLang.configuration) { - const rawConfiguration = this.readJson(path.resolve(pluginPath, rawLang.configuration)); + const rawConfiguration = await this.readJson(path.resolve(pluginPath, rawLang.configuration)); if (rawConfiguration) { const configuration: LanguageConfiguration = { brackets: rawConfiguration.brackets, diff --git a/packages/plugin-ext/src/main/node/errors.spec.ts b/packages/plugin-ext/src/main/node/errors.spec.ts new file mode 100644 index 0000000000000..56a20052ba287 --- /dev/null +++ b/packages/plugin-ext/src/main/node/errors.spec.ts @@ -0,0 +1,37 @@ +// ***************************************************************************** +// Copyright (C) 2023 Arduino SA and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { rejects } from 'assert'; +import { strictEqual } from 'assert/strict'; +import { promises as fs } from 'fs'; +import { v4 } from 'uuid'; +import { isENOENT } from '../../common/errors'; + +describe('errors', () => { + describe('errno-exception', () => { + it('should be ENOENT error', async () => { + await rejects(fs.readFile(v4()), reason => isENOENT(reason)); + }); + + it('should not be ENOENT error (no code)', () => { + strictEqual(isENOENT(new Error('I am not ENOENT')), false); + }); + + it('should not be ENOENT error (other code)', async () => { + await rejects(fs.readdir(__filename), reason => !isENOENT(reason)); + }); + }); +}); diff --git a/packages/plugin-ext/src/main/node/handlers/plugin-theia-directory-handler.ts b/packages/plugin-ext/src/main/node/handlers/plugin-theia-directory-handler.ts index 284c33f1a3bd2..a33fa57a72f23 100644 --- a/packages/plugin-ext/src/main/node/handlers/plugin-theia-directory-handler.ts +++ b/packages/plugin-ext/src/main/node/handlers/plugin-theia-directory-handler.ts @@ -17,27 +17,35 @@ import * as path from 'path'; import * as filenamify from 'filenamify'; import * as fs from '@theia/core/shared/fs-extra'; +import type { URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; +import { Deferred } from '@theia/core/lib/common/promise-util'; import { FileUri } from '@theia/core/lib/node'; import { PluginDeployerDirectoryHandler, PluginDeployerEntry, PluginPackage, PluginDeployerDirectoryHandlerContext, PluginDeployerEntryType, PluginType, PluginIdentifiers } from '../../../common/plugin-protocol'; import { PluginCliContribution } from '../plugin-cli-contribution'; -import { getTempDir } from '../temp-dir-util'; +import { getTempDirPathAsync } from '../temp-dir-util'; @injectable() export class PluginTheiaDirectoryHandler implements PluginDeployerDirectoryHandler { - protected readonly deploymentDirectory = FileUri.create(getTempDir('theia-copied')); + protected readonly deploymentDirectory: Deferred; @inject(PluginCliContribution) protected readonly pluginCli: PluginCliContribution; - accept(resolvedPlugin: PluginDeployerEntry): boolean { + constructor() { + this.deploymentDirectory = new Deferred(); + getTempDirPathAsync('theia-copied') + .then(deploymentDirectory => this.deploymentDirectory.resolve(FileUri.create(deploymentDirectory))); + } + + async accept(resolvedPlugin: PluginDeployerEntry): Promise { console.debug('PluginTheiaDirectoryHandler: accepting plugin with path', resolvedPlugin.path()); // handle only directories - if (resolvedPlugin.isFile()) { + if (await resolvedPlugin.isFile()) { return false; } @@ -47,7 +55,7 @@ export class PluginTheiaDirectoryHandler implements PluginDeployerDirectoryHandl try { let packageJson = resolvedPlugin.getValue('package.json'); if (!packageJson) { - packageJson = fs.readJSONSync(packageJsonPath); + packageJson = await fs.readJSON(packageJsonPath); packageJson.publisher ??= PluginIdentifiers.UNPUBLISHED; resolvedPlugin.storeValue('package.json', packageJson); } @@ -81,11 +89,12 @@ export class PluginTheiaDirectoryHandler implements PluginDeployerDirectoryHandl const origin = entry.originalPath(); const targetDir = await this.getExtensionDir(context); try { - if (fs.existsSync(targetDir) || !entry.path().startsWith(origin)) { + if (await fs.pathExists(targetDir) || !entry.path().startsWith(origin)) { console.log(`[${id}]: already copied.`); } else { console.log(`[${id}]: copying to "${targetDir}"`); - await fs.mkdirp(FileUri.fsPath(this.deploymentDirectory)); + const deploymentDirectory = await this.deploymentDirectory.promise; + await fs.mkdirp(FileUri.fsPath(deploymentDirectory)); await context.copy(origin, targetDir); entry.updatePath(targetDir); if (!this.accept(entry)) { @@ -100,6 +109,7 @@ export class PluginTheiaDirectoryHandler implements PluginDeployerDirectoryHandl } protected async getExtensionDir(context: PluginDeployerDirectoryHandlerContext): Promise { - return FileUri.fsPath(this.deploymentDirectory.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' }))); + const deploymentDirectory = await this.deploymentDirectory.promise; + return FileUri.fsPath(deploymentDirectory.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' }))); } } diff --git a/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts b/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts index 337d60ca4b9a0..28983e18b15af 100644 --- a/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts +++ b/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts @@ -15,8 +15,10 @@ // ***************************************************************************** import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext, PluginType } from '../../../common/plugin-protocol'; -import { injectable, inject } from '@theia/core/shared/inversify'; -import { getTempDir } from '../temp-dir-util'; +import type { URI } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { getTempDirPathAsync } from '../temp-dir-util'; import * as fs from '@theia/core/shared/fs-extra'; import * as filenamify from 'filenamify'; import { FileUri } from '@theia/core/lib/node/file-uri'; @@ -25,13 +27,22 @@ import { PluginTheiaEnvironment } from '../../common/plugin-theia-environment'; @injectable() export class PluginTheiaFileHandler implements PluginDeployerFileHandler { - private readonly systemPluginsDirUri = FileUri.create(getTempDir('theia-unpacked')); + private readonly systemPluginsDirUri: Deferred; @inject(PluginTheiaEnvironment) protected readonly environment: PluginTheiaEnvironment; - accept(resolvedPlugin: PluginDeployerEntry): boolean { - return resolvedPlugin.isFile() && resolvedPlugin.path() !== null && resolvedPlugin.path().endsWith('.theia'); + constructor() { + this.systemPluginsDirUri = new Deferred(); + getTempDirPathAsync('theia-unpacked') + .then(systemPluginsDirPath => this.systemPluginsDirUri.resolve(FileUri.create(systemPluginsDirPath))); + } + + async accept(resolvedPlugin: PluginDeployerEntry): Promise { + if (resolvedPlugin.path() !== null && resolvedPlugin.path().endsWith('.theia')) { + return resolvedPlugin.isFile(); + } + return false; } async handle(context: PluginDeployerFileHandlerContext): Promise { @@ -49,6 +60,7 @@ export class PluginTheiaFileHandler implements PluginDeployerFileHandler { } protected async getPluginDir(context: PluginDeployerFileHandlerContext): Promise { - return FileUri.fsPath(this.systemPluginsDirUri.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' }))); + const systemPluginsDirUri = await this.systemPluginsDirUri.promise; + return FileUri.fsPath(systemPluginsDirUri.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' }))); } } diff --git a/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts b/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts index e9c5918a19ded..d35880e6cb4ad 100644 --- a/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts +++ b/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts @@ -18,7 +18,8 @@ import { injectable, inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import * as path from 'path'; import * as fs from '@theia/core/shared/fs-extra'; -import { readdir, remove } from '@theia/core/shared/fs-extra'; +import { readdir } from 'fs/promises'; +import { remove } from '@theia/core/shared/fs-extra'; import * as crypto from 'crypto'; import { ILogger } from '@theia/core'; import { FileUri } from '@theia/core/lib/node'; @@ -139,15 +140,9 @@ export class PluginPathsServiceImpl implements PluginPathsService { } private async cleanupOldLogs(parentLogsDir: string): Promise { - // @ts-ignore - fs-extra types (Even latest version) is not updated with the `withFileTypes` option. - const dirEntries = await readdir(parentLogsDir, { withFileTypes: true }) as string[]; - // `Dirent` type is defined in @types/node since 10.10.0 - // However, upgrading the @types/node in theia to 10.11 (as defined in engine field) - // Causes other packages to break in compilation, so we are using the infamous `any` type... - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const subDirEntries = dirEntries.filter((dirent: any) => dirent.isDirectory()); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const subDirNames = subDirEntries.map((dirent: any) => dirent.name); + const dirEntries = await readdir(parentLogsDir, { withFileTypes: true }); + const subDirEntries = dirEntries.filter(dirent => dirent.isDirectory()); + const subDirNames = subDirEntries.map(dirent => dirent.name); // We never clean a folder that is not a Theia logs session folder. // Even if it does appears under the `parentLogsDir`... const sessionSubDirNames = subDirNames.filter((dirName: string) => SESSION_TIMESTAMP_PATTERN.test(dirName)); diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-directory-handler-context-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-directory-handler-context-impl.ts index e9258876d7788..3e484fab1ca15 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-directory-handler-context-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-directory-handler-context-impl.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import * as path from 'path'; -import * as fs from '@theia/core/shared/fs-extra'; +import { promises as fs } from 'fs'; import { PluginDeployerEntry, PluginDeployerDirectoryHandlerContext } from '../../common/plugin-protocol'; export class PluginDeployerDirectoryHandlerContextImpl implements PluginDeployerDirectoryHandlerContext { @@ -23,17 +23,17 @@ export class PluginDeployerDirectoryHandlerContextImpl implements PluginDeployer constructor(private readonly pluginDeployerEntry: PluginDeployerEntry) { } async copy(origin: string, target: string): Promise { - const contents = await fs.readdir(origin); - await fs.mkdirp(target); - await Promise.all(contents.map(async item => { + const entries = await fs.readdir(origin, { withFileTypes: true }); + await fs.mkdir(target, { recursive: true }); + await Promise.all(entries.map(async entry => { + const item = entry.name; const itemPath = path.resolve(origin, item); const targetPath = path.resolve(target, item); - const stat = await fs.stat(itemPath); - if (stat.isDirectory()) { + if (entry.isDirectory()) { return this.copy(itemPath, targetPath); } - if (stat.isFile()) { - return new Promise((resolve, reject) => fs.copyFile(itemPath, targetPath, e => e === null ? resolve() : reject(e))); + if (entry.isFile()) { + return fs.copyFile(itemPath, targetPath); } })); } diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-entry-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-entry-impl.ts index 3f1cdbefadd6e..c05912c9ff4de 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-entry-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-entry-impl.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { PluginDeployerEntry, PluginDeployerEntryType, PluginType } from '../../common/plugin-protocol'; -import * as fs from 'fs'; +import { promises as fs } from 'fs'; export class PluginDeployerEntryImpl implements PluginDeployerEntry { @@ -73,17 +73,19 @@ export class PluginDeployerEntryImpl implements PluginDeployerEntry { getChanges(): string[] { return this.changes; } - isFile(): boolean { + async isFile(): Promise { try { - return fs.statSync(this.currentPath).isFile(); - } catch (e) { + const stat = await fs.stat(this.currentPath); + return stat.isFile(); + } catch { return false; } } - isDirectory(): boolean { + async isDirectory(): Promise { try { - return fs.statSync(this.currentPath).isDirectory(); - } catch (e) { + const stat = await fs.stat(this.currentPath); + return stat.isDirectory(); + } catch { return false; } } diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts index 11f9be68ac4fa..18e7783efcade 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts @@ -21,8 +21,8 @@ import * as semver from 'semver'; import { PluginDeployerResolver, PluginDeployerFileHandler, PluginDeployerDirectoryHandler, PluginDeployerEntry, PluginDeployer, PluginDeployerParticipant, PluginDeployerStartContext, - PluginDeployerResolverInit, PluginDeployerFileHandlerContext, - PluginDeployerDirectoryHandlerContext, PluginDeployerEntryType, PluginDeployerHandler, PluginType, UnresolvedPluginEntry, PluginIdentifiers, PluginDeployOptions + PluginDeployerResolverInit, + PluginDeployerEntryType, PluginDeployerHandler, PluginType, UnresolvedPluginEntry, PluginIdentifiers, PluginDeployOptions } from '../../common/plugin-protocol'; import { PluginDeployerEntryImpl } from './plugin-deployer-entry-impl'; import { @@ -295,41 +295,33 @@ export class PluginDeployerImpl implements PluginDeployer { /** * If there are some single files, try to see if we can work on these files (like unpacking it, etc) */ - public async applyFileHandlers(pluginDeployerEntries: PluginDeployerEntry[]): Promise { - const waitPromises: Array> = []; - - pluginDeployerEntries.filter(pluginDeployerEntry => pluginDeployerEntry.isResolved()).map(pluginDeployerEntry => { - this.pluginDeployerFileHandlers.map(pluginFileHandler => { + public async applyFileHandlers(pluginDeployerEntries: PluginDeployerEntry[]): Promise { + const waitPromises = pluginDeployerEntries.filter(pluginDeployerEntry => pluginDeployerEntry.isResolved()).flatMap(pluginDeployerEntry => + this.pluginDeployerFileHandlers.map(async pluginFileHandler => { const proxyPluginDeployerEntry = new ProxyPluginDeployerEntry(pluginFileHandler, (pluginDeployerEntry) as PluginDeployerEntryImpl); - if (pluginFileHandler.accept(proxyPluginDeployerEntry)) { - const pluginDeployerFileHandlerContext: PluginDeployerFileHandlerContext = new PluginDeployerFileHandlerContextImpl(proxyPluginDeployerEntry); - const promise: Promise = pluginFileHandler.handle(pluginDeployerFileHandlerContext); - waitPromises.push(promise); + if (await pluginFileHandler.accept(proxyPluginDeployerEntry)) { + const pluginDeployerFileHandlerContext = new PluginDeployerFileHandlerContextImpl(proxyPluginDeployerEntry); + await pluginFileHandler.handle(pluginDeployerFileHandlerContext); } - }); - - }); - return Promise.all(waitPromises); + }) + ); + await Promise.all(waitPromises); } /** * Check for all registered directories to see if there are some plugins that can be accepted to be deployed. */ - public async applyDirectoryFileHandlers(pluginDeployerEntries: PluginDeployerEntry[]): Promise { - const waitPromises: Array> = []; - - pluginDeployerEntries.filter(pluginDeployerEntry => pluginDeployerEntry.isResolved()).map(pluginDeployerEntry => { - this.pluginDeployerDirectoryHandlers.map(pluginDirectoryHandler => { + public async applyDirectoryFileHandlers(pluginDeployerEntries: PluginDeployerEntry[]): Promise { + const waitPromises = pluginDeployerEntries.filter(pluginDeployerEntry => pluginDeployerEntry.isResolved()).flatMap(pluginDeployerEntry => + this.pluginDeployerDirectoryHandlers.map(async pluginDirectoryHandler => { const proxyPluginDeployerEntry = new ProxyPluginDeployerEntry(pluginDirectoryHandler, (pluginDeployerEntry) as PluginDeployerEntryImpl); - if (pluginDirectoryHandler.accept(proxyPluginDeployerEntry)) { - const pluginDeployerDirectoryHandlerContext: PluginDeployerDirectoryHandlerContext = new PluginDeployerDirectoryHandlerContextImpl(proxyPluginDeployerEntry); - const promise: Promise = pluginDirectoryHandler.handle(pluginDeployerDirectoryHandlerContext); - waitPromises.push(promise); + if (await pluginDirectoryHandler.accept(proxyPluginDeployerEntry)) { + const pluginDeployerDirectoryHandlerContext = new PluginDeployerDirectoryHandlerContextImpl(proxyPluginDeployerEntry); + await pluginDirectoryHandler.handle(pluginDeployerDirectoryHandlerContext); } - }); - - }); - return Promise.all(waitPromises); + }) + ); + await Promise.all(waitPromises); } /** diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-proxy-entry-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-proxy-entry-impl.ts index 93c5b5c067761..fe9f4f5758041 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-proxy-entry-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-proxy-entry-impl.ts @@ -54,11 +54,11 @@ export class ProxyPluginDeployerEntry implements PluginDeployerEntry { return this.delegate.getChanges(); } - isFile(): boolean { + isFile(): Promise { return this.delegate.isFile(); } - isDirectory(): boolean { + isDirectory(): Promise { return this.delegate.isDirectory(); } isResolved(): boolean { diff --git a/packages/plugin-ext/src/main/node/plugin-github-resolver.ts b/packages/plugin-ext/src/main/node/plugin-github-resolver.ts index a0ccb087e941b..27fb6a0bc2d34 100644 --- a/packages/plugin-ext/src/main/node/plugin-github-resolver.ts +++ b/packages/plugin-ext/src/main/node/plugin-github-resolver.ts @@ -16,10 +16,11 @@ import { RequestContext, RequestService } from '@theia/core/shared/@theia/request'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { promises as fs, existsSync, mkdirSync } from 'fs'; -import * as os from 'os'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { promises as fs } from 'fs'; import * as path from 'path'; import { PluginDeployerResolver, PluginDeployerResolverContext } from '../../common'; +import { getTempDirPathAsync } from './temp-dir-util'; /** * Resolver that handle the github: protocol @@ -33,16 +34,21 @@ export class GithubPluginDeployerResolver implements PluginDeployerResolver { private static GITHUB_ENDPOINT = 'https://github.com/'; - private unpackedFolder: string; + private unpackedFolder: Deferred; @inject(RequestService) protected readonly request: RequestService; constructor() { - this.unpackedFolder = path.resolve(os.tmpdir(), 'github-remote'); - if (!existsSync(this.unpackedFolder)) { - mkdirSync(this.unpackedFolder); - } + this.unpackedFolder = new Deferred(); + getTempDirPathAsync('github-remote').then(async unpackedFolder => { + try { + await fs.mkdir(unpackedFolder, { recursive: true }); + this.unpackedFolder.resolve(unpackedFolder); + } catch (err) { + this.unpackedFolder.reject(err); + } + }); } /** @@ -106,7 +112,8 @@ export class GithubPluginDeployerResolver implements PluginDeployerResolver { * Grab the github file specified by the plugin's ID */ protected async grabGithubFile(pluginResolverContext: PluginDeployerResolverContext, orgName: string, repoName: string, filename: string, version: string): Promise { - const unpackedPath = path.resolve(this.unpackedFolder, path.basename(version + filename)); + const unpackedFolder = await this.unpackedFolder.promise; + const unpackedPath = path.resolve(unpackedFolder, path.basename(version + filename)); try { await fs.access(unpackedPath); // use of cache. If file is already there use it directly diff --git a/packages/plugin-ext/src/main/node/plugin-http-resolver.ts b/packages/plugin-ext/src/main/node/plugin-http-resolver.ts index 971ba3d0d8d22..a8b20b4f50a63 100644 --- a/packages/plugin-ext/src/main/node/plugin-http-resolver.ts +++ b/packages/plugin-ext/src/main/node/plugin-http-resolver.ts @@ -16,11 +16,12 @@ import { RequestContext, RequestService } from '@theia/core/shared/@theia/request'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { promises as fs, existsSync, mkdirSync } from 'fs'; -import * as os from 'os'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { promises as fs } from 'fs'; import * as path from 'path'; import * as url from 'url'; import { PluginDeployerResolver, PluginDeployerResolverContext } from '../../common'; +import { getTempDirPathAsync } from './temp-dir-util'; /** * Resolver that handle the http(s): protocol @@ -30,16 +31,21 @@ import { PluginDeployerResolver, PluginDeployerResolverContext } from '../../com @injectable() export class HttpPluginDeployerResolver implements PluginDeployerResolver { - private unpackedFolder: string; + private unpackedFolder: Deferred; @inject(RequestService) protected readonly request: RequestService; constructor() { - this.unpackedFolder = path.resolve(os.tmpdir(), 'http-remote'); - if (!existsSync(this.unpackedFolder)) { - mkdirSync(this.unpackedFolder); - } + this.unpackedFolder = new Deferred(); + getTempDirPathAsync('http-remote').then(async unpackedFolder => { + try { + await fs.mkdir(unpackedFolder, { recursive: true }); + this.unpackedFolder.resolve(unpackedFolder); + } catch (err) { + this.unpackedFolder.reject(err); + } + }); } /** @@ -58,7 +64,8 @@ export class HttpPluginDeployerResolver implements PluginDeployerResolver { const dirname = path.dirname(link.pathname); const basename = path.basename(link.pathname); const filename = dirname.replace(/\W/g, '_') + ('-') + basename; - const unpackedPath = path.resolve(this.unpackedFolder, path.basename(filename)); + const unpackedFolder = await this.unpackedFolder.promise; + const unpackedPath = path.resolve(unpackedFolder, path.basename(filename)); try { await fs.access(unpackedPath); diff --git a/packages/plugin-ext/src/main/node/temp-dir-util.ts b/packages/plugin-ext/src/main/node/temp-dir-util.ts index c0285be99c802..4e5aeb63e80b1 100644 --- a/packages/plugin-ext/src/main/node/temp-dir-util.ts +++ b/packages/plugin-ext/src/main/node/temp-dir-util.ts @@ -15,13 +15,22 @@ // ***************************************************************************** import * as os from 'os'; import * as path from 'path'; -import * as fs from 'fs'; +import { realpathSync, promises as fs } from 'fs'; export function getTempDir(name: string): string { let tempDir = os.tmpdir(); // for mac os 'os.tmpdir()' return symlink, but we need real path if (process.platform === 'darwin') { - tempDir = fs.realpathSync(tempDir); + tempDir = realpathSync(tempDir); + } + return path.resolve(tempDir, name); +} + +export async function getTempDirPathAsync(name: string): Promise { + let tempDir = os.tmpdir(); + // for mac os 'os.tmpdir()' return symlink, but we need real path + if (process.platform === 'darwin') { + tempDir = await fs.realpath(tempDir); } return path.resolve(tempDir, name); } diff --git a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts index b35ff8b168ba9..70f659b355bd0 100644 --- a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts +++ b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts @@ -957,10 +957,10 @@ describe('ripgrep-search-in-workspace-server', function (): void { describe('#extractSearchPathsFromIncludes', function (): void { this.timeout(10000); - it('should not resolve paths from a not absolute / relative pattern', function (): void { + it('should not resolve paths from a not absolute / relative pattern', async () => { const pattern = 'carrots'; const options = { include: [pattern] }; - const searchPaths = ripgrepServer['extractSearchPathsFromIncludes']([rootDirA], options); + const searchPaths = await ripgrepServer['extractSearchPathsFromIncludes']([rootDirA], options); // Same root directory expect(searchPaths.length).equal(1); expect(searchPaths[0]).equal(rootDirA); @@ -970,21 +970,21 @@ describe('#extractSearchPathsFromIncludes', function (): void { expect(options.include[0]).equals(pattern); }); - it('should resolve pattern to path for relative filename', function (): void { + it('should resolve pattern to path for relative filename', async () => { const filename = 'carrots'; const pattern = `./${filename}`; - checkResolvedPathForPattern(pattern, path.join(rootDirA, filename)); + await checkResolvedPathForPattern(pattern, path.join(rootDirA, filename)); }); - it('should resolve relative pattern with sub-folders glob', function (): void { + it('should resolve relative pattern with sub-folders glob', async () => { const filename = 'carrots'; const pattern = `./${filename}/**`; - checkResolvedPathForPattern(pattern, path.join(rootDirA, filename)); + await checkResolvedPathForPattern(pattern, path.join(rootDirA, filename)); }); - it('should resolve absolute path pattern', function (): void { + it('should resolve absolute path pattern', async () => { const pattern = `${rootDirA}/carrots`; - checkResolvedPathForPattern(pattern, pattern); + await checkResolvedPathForPattern(pattern, pattern); }); }); @@ -1064,9 +1064,9 @@ describe('#addGlobArgs', function (): void { }); }); -function checkResolvedPathForPattern(pattern: string, expectedPath: string): void { +async function checkResolvedPathForPattern(pattern: string, expectedPath: string): Promise { const options = { include: [pattern] }; - const searchPaths = ripgrepServer['extractSearchPathsFromIncludes']([rootDirA], options); + const searchPaths = await ripgrepServer['extractSearchPathsFromIncludes']([rootDirA], options); expect(searchPaths.length).equal(1, 'searchPath result should contain exactly one element'); expect(options.include.length).equals(0, 'options.include should be empty'); expect(searchPaths[0]).equal(path.normalize(expectedPath)); diff --git a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts index 499a81db50a73..f22ccaf9ed731 100644 --- a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts +++ b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts @@ -216,7 +216,7 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer { const rootPaths = rootUris.map(root => FileUri.fsPath(root)); // If there are absolute paths in `include` we will remove them and use // those as paths to search from. - const searchPaths = this.extractSearchPathsFromIncludes(rootPaths, options); + const searchPaths = await this.extractSearchPathsFromIncludes(rootPaths, options); options.include = this.replaceRelativeToAbsolute(searchPaths, options.include); options.exclude = this.replaceRelativeToAbsolute(searchPaths, options.exclude); const rgArgs = this.getArgs(options); @@ -388,23 +388,27 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer { * Any pattern that resulted in a valid search path will be removed from the 'include' list as it is * provided as an equivalent search path instead. */ - protected extractSearchPathsFromIncludes(rootPaths: string[], options: SearchInWorkspaceOptions): string[] { + protected async extractSearchPathsFromIncludes(rootPaths: string[], options: SearchInWorkspaceOptions): Promise { if (!options.include) { return rootPaths; } const resolvedPaths = new Set(); - options.include = options.include.filter(pattern => { + const include: string[] = []; + for (const pattern of options.include) { let keep = true; for (const root of rootPaths) { - const absolutePath = this.getAbsolutePathFromPattern(root, pattern); + const absolutePath = await this.getAbsolutePathFromPattern(root, pattern); // undefined means the pattern cannot be converted into an absolute path if (absolutePath) { resolvedPaths.add(absolutePath); keep = false; } } - return keep; - }); + if (keep) { + include.push(pattern); + } + } + options.include = include; return resolvedPaths.size > 0 ? Array.from(resolvedPaths) : rootPaths; @@ -417,7 +421,7 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer { * * @returns undefined if the pattern cannot be converted into an absolute path. */ - protected getAbsolutePathFromPattern(root: string, pattern: string): string | undefined { + protected async getAbsolutePathFromPattern(root: string, pattern: string): Promise { pattern = pattern.replace(/\\/g, '/'); // The pattern is not referring to a single file or folder, i.e. not to be converted if (!path.isAbsolute(pattern) && !pattern.startsWith('./')) { @@ -429,7 +433,7 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer { } // if `pattern` is absolute then `root` will be ignored by `path.resolve()` const targetPath = path.resolve(root, pattern); - if (fs.existsSync(targetPath)) { + if (await fs.pathExists(targetPath)) { return targetPath; } return undefined; diff --git a/packages/workspace/src/node/default-workspace-server.ts b/packages/workspace/src/node/default-workspace-server.ts index b97ff3a1fbde1..ff03c57657ea8 100644 --- a/packages/workspace/src/node/default-workspace-server.ts +++ b/packages/workspace/src/node/default-workspace-server.ts @@ -25,6 +25,7 @@ import { Deferred } from '@theia/core/lib/common/promise-util'; import { WorkspaceServer, UntitledWorkspaceService } from '../common'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import URI from '@theia/core/lib/common/uri'; +import { notEmpty } from '@theia/core'; @injectable() export class WorkspaceCliContribution implements CliContribution { @@ -150,22 +151,17 @@ export class DefaultWorkspaceServer implements WorkspaceServer, BackendApplicati } async getRecentWorkspaces(): Promise { - const listUri: string[] = []; const data = await this.readRecentWorkspacePathsFromUserHome(); if (data && data.recentRoots) { - data.recentRoots.forEach(element => { - if (element.length > 0) { - if (this.workspaceStillExist(element)) { - listUri.push(element); - } - } - }); + const allRootUris = await Promise.all(data.recentRoots.map(async element => + element && await this.workspaceStillExist(element) ? element : undefined)); + return allRootUris.filter(notEmpty); } - return listUri; + return []; } - protected workspaceStillExist(workspaceRootUri: string): boolean { - return fs.pathExistsSync(FileUri.fsPath(workspaceRootUri)); + protected async workspaceStillExist(workspaceRootUri: string): Promise { + return fs.pathExists(FileUri.fsPath(workspaceRootUri)); } protected async getWorkspaceURIFromCli(): Promise {