Skip to content

Commit

Permalink
fix: revise vscode extension directories
Browse files Browse the repository at this point in the history
This patch eliminates the use of the temporary directories
vscode-unpacked and vscode-copied for installing and deploying
vscode extensions.

The file-handler and directory handler for vscode extensions
have been adjusted accordingly:

* A temporary directory is now only used for downloading extensions from
  the registry. This temporary directory is now user-specific and
  resides within the configdir (i.e., per default in the user's home:
  /.theia/tmp/) to avoid clashes and permission issues on multi-user
  operating systems that share temporary directories, such as Linux or
  BSDs. Having this temporary directory in a location that is
  configurable by the user also seems the more sensible approach when
  extensions are considered confidential data.

* $configDir/deployedPlugins replaces our volatile /tmp/vscode-copied
  deployment directory. Having a more permanent way of handling
  installed extensions should improve startup time and reduce issues
  with multiple instances running in parallel.

* The local file resolver unpacks the vsix file from the temp dir into
  $configDir/deployedPlugins/<extension-id>.

* The simplified directory handler loads unpacked extensions directly
  from $configDir/deployedPlugins/<extension-id>.

* We use $configDir/extensions as a location for the user to drop vsix
  files that will be installed to the deployment location automatically
  on startup. We do not manage or remove files within
  $configDir/extensions.

Overall, this should improve the stability on systems with shared temp
dir locations and reduce the startup of the first application start
after a reboot.

Contributed on behalf of STMicroelectronics

Signed-off-by: Olaf Lessenich <[email protected]>
  • Loading branch information
xai committed Dec 18, 2023
1 parent de4c424 commit cc91418
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 122 deletions.
1 change: 1 addition & 0 deletions packages/plugin-ext-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@theia/typehierarchy": "1.44.0",
"@theia/userstorage": "1.44.0",
"@theia/workspace": "1.44.0",
"decompress": "^4.2.1",
"filenamify": "^4.1.0"
},
"publishConfig": {
Expand Down
33 changes: 28 additions & 5 deletions packages/plugin-ext-vscode/src/common/plugin-vscode-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,36 @@ export class PluginVSCodeEnvironment {
@inject(EnvVariablesServer)
protected readonly environments: EnvVariablesServer;

protected _extensionsDirUri: URI | undefined;
async getExtensionsDirUri(): Promise<URI> {
if (!this._extensionsDirUri) {
protected _userExtensionsDirUri: URI | undefined;
protected _deployedPluginsUri: URI | undefined;
protected _tmpDirUri: URI | undefined;

async getUserExtensionsDirUri(): Promise<URI> {
if (!this._userExtensionsDirUri) {
const configDir = new URI(await this.environments.getConfigDirUri());
this._userExtensionsDirUri = configDir.resolve('extensions');
}
return this._userExtensionsDirUri;
}

async getDeploymentDirUri(): Promise<URI> {
if (!this._deployedPluginsUri) {
const configDir = new URI(await this.environments.getConfigDirUri());
this._extensionsDirUri = configDir.resolve('extensions');
this._deployedPluginsUri = configDir.resolve('deployedPlugins');
}
return this._extensionsDirUri;
return this._deployedPluginsUri;
}

async getTempDirUri(prefix?: string): Promise<URI> {
if (!this._tmpDirUri) {
const configDir: URI = new URI(await this.environments.getConfigDirUri());
this._tmpDirUri = configDir.resolve('tmp');
}

if (prefix) {
return this._tmpDirUri.resolve(prefix);
}

return this._tmpDirUri;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import * as fs from '@theia/core/shared/fs-extra';
import * as path from 'path';
import { inject, injectable } from '@theia/core/shared/inversify';
import { FileUri } from '@theia/core/lib/node';
import { PluginDeployerResolverContext } from '@theia/plugin-ext';
import { LocalPluginDeployerResolver } from '@theia/plugin-ext/lib/main/node/resolvers/local-plugin-deployer-resolver';
import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment';
import { isVSCodePluginFile } from './plugin-vscode-file-handler';
import { existsInDeploymentDir, unpackToDeploymentDir } from './plugin-vscode-utils';

@injectable()
export class LocalVSIXFilePluginDeployerResolver extends LocalPluginDeployerResolver {
static LOCAL_FILE = 'local-file';
static FILE_EXTENSION = '.vsix';

@inject(PluginVSCodeEnvironment) protected readonly environment: PluginVSCodeEnvironment;

Expand All @@ -38,28 +38,14 @@ export class LocalVSIXFilePluginDeployerResolver extends LocalPluginDeployerReso
}

async resolveFromLocalPath(pluginResolverContext: PluginDeployerResolverContext, localPath: string): Promise<void> {
const fileName = path.basename(localPath);
const pathInUserExtensionsDirectory = await this.ensureDiscoverability(localPath);
pluginResolverContext.addPlugin(fileName, pathInUserExtensionsDirectory);
}
const extensionId = path.basename(localPath, LocalVSIXFilePluginDeployerResolver.FILE_EXTENSION);

/**
* Ensures that a user-installed plugin file is transferred to the user extension folder.
*/
protected async ensureDiscoverability(localPath: string): Promise<string> {
const userExtensionsDir = await this.environment.getExtensionsDirUri();
if (!userExtensionsDir.isEqualOrParent(FileUri.create(localPath))) {
try {
const newPath = FileUri.fsPath(userExtensionsDir.resolve(path.basename(localPath)));
await fs.mkdirp(FileUri.fsPath(userExtensionsDir));
await new Promise<void>((resolve, reject) => {
fs.copyFile(localPath, newPath, error => error ? reject(error) : resolve());
});
return newPath;
} catch (e) {
console.warn(`Problem copying plugin at ${localPath}:`, e);
}
if (await existsInDeploymentDir(this.environment, extensionId)) {
console.log(`[${pluginResolverContext.getOriginId()}]: Target dir already exists in plugin deployment dir`);
return;
}
return localPath;

const extensionDeploymentDir = await unpackToDeploymentDir(this.environment, localPath, extensionId);
pluginResolverContext.addPlugin(extensionId, extensionDeploymentDir);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
// *****************************************************************************

import { injectable, inject } from '@theia/core/shared/inversify';
import * as fs from '@theia/core/shared/fs-extra';
import { FileUri } from '@theia/core/lib/node';
import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment';
import { PluginDeployerParticipant, PluginDeployerStartContext } from '@theia/plugin-ext/lib/common/plugin-protocol';
import { LocalVSIXFilePluginDeployerResolver } from './local-vsix-file-plugin-deployer-resolver';

@injectable()
export class PluginVSCodeDeployerParticipant implements PluginDeployerParticipant {
Expand All @@ -25,8 +28,21 @@ export class PluginVSCodeDeployerParticipant implements PluginDeployerParticipan
protected readonly environments: PluginVSCodeEnvironment;

async onWillStart(context: PluginDeployerStartContext): Promise<void> {
const extensionsDirUri = await this.environments.getExtensionsDirUri();
context.userEntries.push(extensionsDirUri.withScheme('local-dir').toString());
}
const extensionDeploymentDirUri = await this.environments.getDeploymentDirUri();
context.userEntries.push(extensionDeploymentDirUri.withScheme('local-dir').toString());

const userExtensionDirUri = await this.environments.getUserExtensionsDirUri();
const userExtensionDirPath = FileUri.fsPath(userExtensionDirUri);

if (await fs.pathExists(userExtensionDirPath)) {
const files = await fs.readdir(userExtensionDirPath);
for (const file of files) {
if (file.endsWith(LocalVSIXFilePluginDeployerResolver.FILE_EXTENSION)) {
const extensionUri = userExtensionDirUri.resolve(file).withScheme('local-file').toString();
console.log(`found drop-in extension "${extensionUri}"`);
context.userEntries.push(extensionUri);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,16 @@
// *****************************************************************************

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 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
PluginDeployerEntryType, PluginPackage, PluginIdentifiers
} from '@theia/plugin-ext';
import { FileUri } from '@theia/core/lib/node';
import { PluginCliContribution } from '@theia/plugin-ext/lib/main/node/plugin-cli-contribution';
import { TMP_DIR_PREFIX } from './plugin-vscode-utils';

@injectable()
export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHandler {
Expand All @@ -35,14 +33,12 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand

@inject(PluginCliContribution) protected readonly pluginCli: PluginCliContribution;

constructor() {
this.deploymentDirectory = new Deferred();
getTempDirPathAsync('vscode-copied')
.then(deploymentDirectoryPath => this.deploymentDirectory.resolve(FileUri.create(deploymentDirectoryPath)));
}

async accept(plugin: PluginDeployerEntry): Promise<boolean> {
console.debug(`Resolving "${plugin.id()}" as a VS Code extension...`);
if (plugin.path().startsWith(TMP_DIR_PREFIX)) {
// avoid adding corrupted plugins from temporary directories
return false;
}
return this.attemptResolution(plugin);
}

Expand All @@ -62,7 +58,6 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand
}

async handle(context: PluginDeployerDirectoryHandlerContext): Promise<void> {
await this.copyDirectory(context);
const types: PluginDeployerEntryType[] = [];
const packageJson: PluginPackage = context.pluginEntry().getValue('package.json');
if (packageJson.browser) {
Expand All @@ -74,33 +69,6 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand
context.pluginEntry().accept(...types);
}

protected async copyDirectory(context: PluginDeployerDirectoryHandlerContext): Promise<void> {
if (this.pluginCli.copyUncompressedPlugins() && context.pluginEntry().type === PluginType.User) {
const entry = context.pluginEntry();
const id = entry.id();
const pathToRestore = entry.path();
const origin = entry.originalPath();
const targetDir = await this.getExtensionDir(context);
try {
if (await fs.pathExists(targetDir) || !entry.path().startsWith(origin)) {
console.log(`[${id}]: already copied.`);
} else {
console.log(`[${id}]: copying to "${targetDir}"`);
const deploymentDirectory = await this.deploymentDirectory.promise;
await fs.mkdirp(FileUri.fsPath(deploymentDirectory));
await context.copy(origin, targetDir);
entry.updatePath(targetDir);
if (!this.deriveMetadata(entry)) {
throw new Error('Unable to resolve plugin metadata after copying');
}
}
} catch (e) {
console.warn(`[${id}]: Error when copying.`, e);
entry.updatePath(pathToRestore);
}
}
}

protected async resolveFromSources(plugin: PluginDeployerEntry): Promise<boolean> {
const pluginPath = plugin.path();
const pck = await this.requirePackage(pluginPath);
Expand Down Expand Up @@ -152,9 +120,4 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand
return undefined;
}
}

protected async getExtensionDir(context: PluginDeployerDirectoryHandlerContext): Promise<string> {
const deploymentDirectory = await this.deploymentDirectory.promise;
return FileUri.fsPath(deploymentDirectory.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' })));
}
}
61 changes: 20 additions & 41 deletions packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,21 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext, PluginType } from '@theia/plugin-ext';
import * as fs from '@theia/core/shared/fs-extra';
import * as path from 'path';
import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext } from '@theia/plugin-ext';
import * as filenamify from 'filenamify';
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 * as fs from '@theia/core/shared/fs-extra';
import { FileUri } from '@theia/core/lib/node';
import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { unpackToDeploymentDir } from './plugin-vscode-utils';

export const isVSCodePluginFile = (pluginPath?: string) => Boolean(pluginPath && (pluginPath.endsWith('.vsix') || pluginPath.endsWith('.tgz')));

@injectable()
export class PluginVsCodeFileHandler implements PluginDeployerFileHandler {

@inject(PluginVSCodeEnvironment)
protected readonly environment: PluginVSCodeEnvironment;

private readonly systemExtensionsDirUri: Deferred<URI>;

constructor() {
this.systemExtensionsDirUri = new Deferred();
getTempDirPathAsync('vscode-unpacked')
.then(systemExtensionsDirPath => this.systemExtensionsDirUri.resolve(FileUri.create(systemExtensionsDirPath)));
}

async accept(resolvedPlugin: PluginDeployerEntry): Promise<boolean> {
return resolvedPlugin.isFile().then(file => {
if (!file) {
Expand All @@ -51,33 +39,24 @@ export class PluginVsCodeFileHandler implements PluginDeployerFileHandler {
}

async handle(context: PluginDeployerFileHandlerContext): Promise<void> {
const id = context.pluginEntry().id();
const extensionDir = await this.getExtensionDir(context);
console.log(`[${id}]: trying to decompress into "${extensionDir}"...`);
if (context.pluginEntry().type === PluginType.User && await fs.pathExists(extensionDir)) {
console.log(`[${id}]: already found`);
context.pluginEntry().updatePath(extensionDir);
return;
}
await this.decompress(extensionDir, context);
console.log(`[${id}]: decompressed`);
context.pluginEntry().updatePath(extensionDir);
}

protected async getExtensionDir(context: PluginDeployerFileHandlerContext): Promise<string> {
const systemExtensionsDirUri = await this.systemExtensionsDirUri.promise;
return FileUri.fsPath(systemExtensionsDirUri.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' })));
}

protected async decompress(extensionDir: string, context: PluginDeployerFileHandlerContext): Promise<void> {
await context.unzip(context.pluginEntry().path(), extensionDir);
if (context.pluginEntry().path().endsWith('.tgz')) {
const extensionPath = path.join(extensionDir, 'package');
const vscodeNodeModulesPath = path.join(extensionPath, 'vscode_node_modules.zip');
if (await fs.pathExists(vscodeNodeModulesPath)) {
await context.unzip(vscodeNodeModulesPath, path.join(extensionPath, 'node_modules'));
const id = this.getNormalizedExtensionId(context.pluginEntry().id());
const extensionDeploymentDir = await unpackToDeploymentDir(this.environment, context.pluginEntry().path(), id);
context.pluginEntry().updatePath(extensionDeploymentDir);
console.log(`root path: ${context.pluginEntry().rootPath}`);
const originalPath = context.pluginEntry().originalPath();
if (originalPath && originalPath !== extensionDeploymentDir) {
const tempDirUri = await this.environment.getTempDirUri();
if (originalPath.startsWith(FileUri.fsPath(tempDirUri))) {
try {
await fs.remove(FileUri.fsPath(originalPath));
} catch (e) {
console.error(`[${id}]: failed to remove temporary files: "${originalPath}"`, e);
}
}
}
}

protected getNormalizedExtensionId(pluginId: string): string {
return filenamify(pluginId, { replacement: '_' }).replace(/\.vsix$/, '');
}
}
Loading

0 comments on commit cc91418

Please sign in to comment.