Skip to content

Commit cc91418

Browse files
committed
fix: revise vscode extension directories
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]>
1 parent de4c424 commit cc91418

File tree

8 files changed

+202
-122
lines changed

8 files changed

+202
-122
lines changed

packages/plugin-ext-vscode/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@theia/typehierarchy": "1.44.0",
1717
"@theia/userstorage": "1.44.0",
1818
"@theia/workspace": "1.44.0",
19+
"decompress": "^4.2.1",
1920
"filenamify": "^4.1.0"
2021
},
2122
"publishConfig": {

packages/plugin-ext-vscode/src/common/plugin-vscode-environment.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,36 @@ export class PluginVSCodeEnvironment {
2424
@inject(EnvVariablesServer)
2525
protected readonly environments: EnvVariablesServer;
2626

27-
protected _extensionsDirUri: URI | undefined;
28-
async getExtensionsDirUri(): Promise<URI> {
29-
if (!this._extensionsDirUri) {
27+
protected _userExtensionsDirUri: URI | undefined;
28+
protected _deployedPluginsUri: URI | undefined;
29+
protected _tmpDirUri: URI | undefined;
30+
31+
async getUserExtensionsDirUri(): Promise<URI> {
32+
if (!this._userExtensionsDirUri) {
33+
const configDir = new URI(await this.environments.getConfigDirUri());
34+
this._userExtensionsDirUri = configDir.resolve('extensions');
35+
}
36+
return this._userExtensionsDirUri;
37+
}
38+
39+
async getDeploymentDirUri(): Promise<URI> {
40+
if (!this._deployedPluginsUri) {
3041
const configDir = new URI(await this.environments.getConfigDirUri());
31-
this._extensionsDirUri = configDir.resolve('extensions');
42+
this._deployedPluginsUri = configDir.resolve('deployedPlugins');
3243
}
33-
return this._extensionsDirUri;
44+
return this._deployedPluginsUri;
3445
}
3546

47+
async getTempDirUri(prefix?: string): Promise<URI> {
48+
if (!this._tmpDirUri) {
49+
const configDir: URI = new URI(await this.environments.getConfigDirUri());
50+
this._tmpDirUri = configDir.resolve('tmp');
51+
}
52+
53+
if (prefix) {
54+
return this._tmpDirUri.resolve(prefix);
55+
}
56+
57+
return this._tmpDirUri;
58+
}
3659
}

packages/plugin-ext-vscode/src/node/local-vsix-file-plugin-deployer-resolver.ts

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616

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

2625
@injectable()
2726
export class LocalVSIXFilePluginDeployerResolver extends LocalPluginDeployerResolver {
2827
static LOCAL_FILE = 'local-file';
28+
static FILE_EXTENSION = '.vsix';
2929

3030
@inject(PluginVSCodeEnvironment) protected readonly environment: PluginVSCodeEnvironment;
3131

@@ -38,28 +38,14 @@ export class LocalVSIXFilePluginDeployerResolver extends LocalPluginDeployerReso
3838
}
3939

4040
async resolveFromLocalPath(pluginResolverContext: PluginDeployerResolverContext, localPath: string): Promise<void> {
41-
const fileName = path.basename(localPath);
42-
const pathInUserExtensionsDirectory = await this.ensureDiscoverability(localPath);
43-
pluginResolverContext.addPlugin(fileName, pathInUserExtensionsDirectory);
44-
}
41+
const extensionId = path.basename(localPath, LocalVSIXFilePluginDeployerResolver.FILE_EXTENSION);
4542

46-
/**
47-
* Ensures that a user-installed plugin file is transferred to the user extension folder.
48-
*/
49-
protected async ensureDiscoverability(localPath: string): Promise<string> {
50-
const userExtensionsDir = await this.environment.getExtensionsDirUri();
51-
if (!userExtensionsDir.isEqualOrParent(FileUri.create(localPath))) {
52-
try {
53-
const newPath = FileUri.fsPath(userExtensionsDir.resolve(path.basename(localPath)));
54-
await fs.mkdirp(FileUri.fsPath(userExtensionsDir));
55-
await new Promise<void>((resolve, reject) => {
56-
fs.copyFile(localPath, newPath, error => error ? reject(error) : resolve());
57-
});
58-
return newPath;
59-
} catch (e) {
60-
console.warn(`Problem copying plugin at ${localPath}:`, e);
61-
}
43+
if (await existsInDeploymentDir(this.environment, extensionId)) {
44+
console.log(`[${pluginResolverContext.getOriginId()}]: Target dir already exists in plugin deployment dir`);
45+
return;
6246
}
63-
return localPath;
47+
48+
const extensionDeploymentDir = await unpackToDeploymentDir(this.environment, localPath, extensionId);
49+
pluginResolverContext.addPlugin(extensionId, extensionDeploymentDir);
6450
}
6551
}

packages/plugin-ext-vscode/src/node/plugin-vscode-deployer-participant.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
// *****************************************************************************
1616

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

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

2730
async onWillStart(context: PluginDeployerStartContext): Promise<void> {
28-
const extensionsDirUri = await this.environments.getExtensionsDirUri();
29-
context.userEntries.push(extensionsDirUri.withScheme('local-dir').toString());
30-
}
31+
const extensionDeploymentDirUri = await this.environments.getDeploymentDirUri();
32+
context.userEntries.push(extensionDeploymentDirUri.withScheme('local-dir').toString());
33+
34+
const userExtensionDirUri = await this.environments.getUserExtensionsDirUri();
35+
const userExtensionDirPath = FileUri.fsPath(userExtensionDirUri);
3136

37+
if (await fs.pathExists(userExtensionDirPath)) {
38+
const files = await fs.readdir(userExtensionDirPath);
39+
for (const file of files) {
40+
if (file.endsWith(LocalVSIXFilePluginDeployerResolver.FILE_EXTENSION)) {
41+
const extensionUri = userExtensionDirUri.resolve(file).withScheme('local-file').toString();
42+
console.log(`found drop-in extension "${extensionUri}"`);
43+
context.userEntries.push(extensionUri);
44+
}
45+
}
46+
}
47+
}
3248
}

packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts

Lines changed: 6 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,16 @@
1515
// *****************************************************************************
1616

1717
import * as path from 'path';
18-
import * as filenamify from 'filenamify';
1918
import * as fs from '@theia/core/shared/fs-extra';
2019
import { inject, injectable } from '@theia/core/shared/inversify';
2120
import type { RecursivePartial, URI } from '@theia/core';
2221
import { Deferred, firstTrue } from '@theia/core/lib/common/promise-util';
23-
import { getTempDirPathAsync } from '@theia/plugin-ext/lib/main/node/temp-dir-util';
2422
import {
2523
PluginDeployerDirectoryHandler, PluginDeployerEntry, PluginDeployerDirectoryHandlerContext,
26-
PluginDeployerEntryType, PluginPackage, PluginType, PluginIdentifiers
24+
PluginDeployerEntryType, PluginPackage, PluginIdentifiers
2725
} from '@theia/plugin-ext';
28-
import { FileUri } from '@theia/core/lib/node';
2926
import { PluginCliContribution } from '@theia/plugin-ext/lib/main/node/plugin-cli-contribution';
27+
import { TMP_DIR_PREFIX } from './plugin-vscode-utils';
3028

3129
@injectable()
3230
export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHandler {
@@ -35,14 +33,12 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand
3533

3634
@inject(PluginCliContribution) protected readonly pluginCli: PluginCliContribution;
3735

38-
constructor() {
39-
this.deploymentDirectory = new Deferred();
40-
getTempDirPathAsync('vscode-copied')
41-
.then(deploymentDirectoryPath => this.deploymentDirectory.resolve(FileUri.create(deploymentDirectoryPath)));
42-
}
43-
4436
async accept(plugin: PluginDeployerEntry): Promise<boolean> {
4537
console.debug(`Resolving "${plugin.id()}" as a VS Code extension...`);
38+
if (plugin.path().startsWith(TMP_DIR_PREFIX)) {
39+
// avoid adding corrupted plugins from temporary directories
40+
return false;
41+
}
4642
return this.attemptResolution(plugin);
4743
}
4844

@@ -62,7 +58,6 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand
6258
}
6359

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

77-
protected async copyDirectory(context: PluginDeployerDirectoryHandlerContext): Promise<void> {
78-
if (this.pluginCli.copyUncompressedPlugins() && context.pluginEntry().type === PluginType.User) {
79-
const entry = context.pluginEntry();
80-
const id = entry.id();
81-
const pathToRestore = entry.path();
82-
const origin = entry.originalPath();
83-
const targetDir = await this.getExtensionDir(context);
84-
try {
85-
if (await fs.pathExists(targetDir) || !entry.path().startsWith(origin)) {
86-
console.log(`[${id}]: already copied.`);
87-
} else {
88-
console.log(`[${id}]: copying to "${targetDir}"`);
89-
const deploymentDirectory = await this.deploymentDirectory.promise;
90-
await fs.mkdirp(FileUri.fsPath(deploymentDirectory));
91-
await context.copy(origin, targetDir);
92-
entry.updatePath(targetDir);
93-
if (!this.deriveMetadata(entry)) {
94-
throw new Error('Unable to resolve plugin metadata after copying');
95-
}
96-
}
97-
} catch (e) {
98-
console.warn(`[${id}]: Error when copying.`, e);
99-
entry.updatePath(pathToRestore);
100-
}
101-
}
102-
}
103-
10472
protected async resolveFromSources(plugin: PluginDeployerEntry): Promise<boolean> {
10573
const pluginPath = plugin.path();
10674
const pck = await this.requirePackage(pluginPath);
@@ -152,9 +120,4 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand
152120
return undefined;
153121
}
154122
}
155-
156-
protected async getExtensionDir(context: PluginDeployerDirectoryHandlerContext): Promise<string> {
157-
const deploymentDirectory = await this.deploymentDirectory.promise;
158-
return FileUri.fsPath(deploymentDirectory.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' })));
159-
}
160123
}

packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts

Lines changed: 20 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,21 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616

17-
import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext, PluginType } from '@theia/plugin-ext';
18-
import * as fs from '@theia/core/shared/fs-extra';
19-
import * as path from 'path';
17+
import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext } from '@theia/plugin-ext';
2018
import * as filenamify from 'filenamify';
21-
import type { URI } from '@theia/core';
2219
import { inject, injectable } from '@theia/core/shared/inversify';
23-
import { Deferred } from '@theia/core/lib/common/promise-util';
24-
import { getTempDirPathAsync } from '@theia/plugin-ext/lib/main/node/temp-dir-util';
20+
import * as fs from '@theia/core/shared/fs-extra';
21+
import { FileUri } from '@theia/core/lib/node';
2522
import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment';
26-
import { FileUri } from '@theia/core/lib/node/file-uri';
23+
import { unpackToDeploymentDir } from './plugin-vscode-utils';
2724

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

3027
@injectable()
3128
export class PluginVsCodeFileHandler implements PluginDeployerFileHandler {
32-
3329
@inject(PluginVSCodeEnvironment)
3430
protected readonly environment: PluginVSCodeEnvironment;
3531

36-
private readonly systemExtensionsDirUri: Deferred<URI>;
37-
38-
constructor() {
39-
this.systemExtensionsDirUri = new Deferred();
40-
getTempDirPathAsync('vscode-unpacked')
41-
.then(systemExtensionsDirPath => this.systemExtensionsDirUri.resolve(FileUri.create(systemExtensionsDirPath)));
42-
}
43-
4432
async accept(resolvedPlugin: PluginDeployerEntry): Promise<boolean> {
4533
return resolvedPlugin.isFile().then(file => {
4634
if (!file) {
@@ -51,33 +39,24 @@ export class PluginVsCodeFileHandler implements PluginDeployerFileHandler {
5139
}
5240

5341
async handle(context: PluginDeployerFileHandlerContext): Promise<void> {
54-
const id = context.pluginEntry().id();
55-
const extensionDir = await this.getExtensionDir(context);
56-
console.log(`[${id}]: trying to decompress into "${extensionDir}"...`);
57-
if (context.pluginEntry().type === PluginType.User && await fs.pathExists(extensionDir)) {
58-
console.log(`[${id}]: already found`);
59-
context.pluginEntry().updatePath(extensionDir);
60-
return;
61-
}
62-
await this.decompress(extensionDir, context);
63-
console.log(`[${id}]: decompressed`);
64-
context.pluginEntry().updatePath(extensionDir);
65-
}
66-
67-
protected async getExtensionDir(context: PluginDeployerFileHandlerContext): Promise<string> {
68-
const systemExtensionsDirUri = await this.systemExtensionsDirUri.promise;
69-
return FileUri.fsPath(systemExtensionsDirUri.resolve(filenamify(context.pluginEntry().id(), { replacement: '_' })));
70-
}
71-
72-
protected async decompress(extensionDir: string, context: PluginDeployerFileHandlerContext): Promise<void> {
73-
await context.unzip(context.pluginEntry().path(), extensionDir);
74-
if (context.pluginEntry().path().endsWith('.tgz')) {
75-
const extensionPath = path.join(extensionDir, 'package');
76-
const vscodeNodeModulesPath = path.join(extensionPath, 'vscode_node_modules.zip');
77-
if (await fs.pathExists(vscodeNodeModulesPath)) {
78-
await context.unzip(vscodeNodeModulesPath, path.join(extensionPath, 'node_modules'));
42+
const id = this.getNormalizedExtensionId(context.pluginEntry().id());
43+
const extensionDeploymentDir = await unpackToDeploymentDir(this.environment, context.pluginEntry().path(), id);
44+
context.pluginEntry().updatePath(extensionDeploymentDir);
45+
console.log(`root path: ${context.pluginEntry().rootPath}`);
46+
const originalPath = context.pluginEntry().originalPath();
47+
if (originalPath && originalPath !== extensionDeploymentDir) {
48+
const tempDirUri = await this.environment.getTempDirUri();
49+
if (originalPath.startsWith(FileUri.fsPath(tempDirUri))) {
50+
try {
51+
await fs.remove(FileUri.fsPath(originalPath));
52+
} catch (e) {
53+
console.error(`[${id}]: failed to remove temporary files: "${originalPath}"`, e);
54+
}
7955
}
8056
}
8157
}
8258

59+
protected getNormalizedExtensionId(pluginId: string): string {
60+
return filenamify(pluginId, { replacement: '_' }).replace(/\.vsix$/, '');
61+
}
8362
}

0 commit comments

Comments
 (0)