Skip to content

Commit

Permalink
fix: use path in user home as extension dir
Browse files Browse the repository at this point in the history
* Instead of unpacking vsix files to a temporary directory,
  they are now unpacked to the user extension dir, which is by default
  <userhome>/.theia/extensions/<extension-id>.
  This avoids redundant unpacking after the temp directory is deleted.

* If the extension is downloaded from a registry, the vsix file is
  downloaded to a temporary location and unpacked into the user
  extension dir from there.

* The temporary deployment directory is no longer used. This should
  speed up Theia's start after the temp dir location has been cleaned
  (e.g., after a reboot).

Signed-off-by: Olaf Lessenich <[email protected]>
  • Loading branch information
xai committed Dec 15, 2023
1 parent de4c424 commit 84ecd86
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 80 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
36 changes: 36 additions & 0 deletions packages/plugin-ext-vscode/src/node/plugin-vscode-decompress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// *****************************************************************************
// Copyright (C) 2023 EclipseSource 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 * as decompress from 'decompress';
import * as path from 'path';
import * as fs from '@theia/core/shared/fs-extra';

export async function decompressExtension(sourcePath: string, destPath: string): Promise<boolean> {
try {
await decompress(sourcePath, destPath);
if (sourcePath.endsWith('.tgz')) {
const extensionPath = path.join(destPath, 'package');
const vscodeNodeModulesPath = path.join(extensionPath, 'vscode_node_modules.zip');
if (await fs.pathExists(vscodeNodeModulesPath)) {
await decompress(vscodeNodeModulesPath, path.join(extensionPath, 'node_modules'));
}
}
return true;
} catch (error) {
console.error(`Failed to decompress ${sourcePath} to ${destPath}: ${error}`);
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,14 @@
// *****************************************************************************

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';

@injectable()
Expand All @@ -35,11 +32,6 @@ 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...`);
Expand All @@ -62,7 +54,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 +65,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 +116,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: '_' })));
}
}
58 changes: 27 additions & 31 deletions packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,11 @@

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

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

Expand All @@ -33,14 +30,6 @@ 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 @@ -52,31 +41,38 @@ 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`);
try {
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}]: extension is already unpacked`);
context.pluginEntry().updatePath(extensionDir);
return;
}

if (!await decompressExtension(context.pluginEntry().path(), extensionDir)) {
console.error(`[${id}]: decompression failed`);
return;
}

console.log(`[${id}]: decompressed`);
context.pluginEntry().updatePath(extensionDir);
} catch (error) {
console.error(`[${id}]: error while decompressing`, error);
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'));
}
try {
const userExtensionsDirUri = await this.environment.getExtensionsDirUri();
const normalizedExtensionId = filenamify(context.pluginEntry().id(), { replacement: '_' });
const extensionDirPath = FileUri.fsPath(userExtensionsDirUri.resolve(normalizedExtensionId));
return extensionDirPath;
} catch (error) {
console.error('Error resolving extension directory:', error);
throw error;
}
}

Expand Down
23 changes: 16 additions & 7 deletions packages/vsx-registry/src/node/vsx-extension-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import * as os from 'os';
import * as path from 'path';
import * as semver from 'semver';
import * as fs from '@theia/core/shared/fs-extra';
import { injectable, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { PluginDeployerHandler, PluginDeployerResolver, PluginDeployerResolverContext, PluginDeployOptions, PluginIdentifiers } from '@theia/plugin-ext/lib/common/plugin-protocol';
import { getTempDirPathAsync } from '@theia/plugin-ext/lib/main/node/temp-dir-util';
import { VSCodeExtensionUri } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-uri';
import { OVSXClientProvider } from '../common/ovsx-client-provider';
import { OVSXApiFilter, VSXExtensionRaw } from '@theia/ovsx-client';
Expand Down Expand Up @@ -74,16 +76,23 @@ export class VSXExtensionResolver implements PluginDeployerResolver {
return;
}
}
const downloadPath = (await this.environment.getExtensionsDirUri()).path.fsPath();
await fs.ensureDir(downloadPath);
const extensionPath = path.resolve(downloadPath, path.basename(downloadUrl));
console.log(`[${resolvedId}]: trying to download from "${downloadUrl}"...`, 'to path', downloadPath);
if (!await this.download(downloadUrl, extensionPath)) {
const downloadDir = await this.getTempDir(resolvedId, 'download');
await fs.ensureDir(downloadDir);
const downloadedExtensionPath = path.resolve(downloadDir, path.basename(downloadUrl));
console.log(`[${resolvedId}]: trying to download from "${downloadUrl}"...`, 'to path', downloadDir);
if (!await this.download(downloadUrl, downloadedExtensionPath)) {
console.log(`[${resolvedId}]: not found`);
return;
}
console.log(`[${resolvedId}]: downloaded to ${extensionPath}"`);
context.addPlugin(resolvedId, extensionPath);
console.log(`[${resolvedId}]: downloaded to ${downloadedExtensionPath}"`);
context.addPlugin(resolvedId, downloadedExtensionPath);
}

protected async getTempDir(id: string, type: string): Promise<string> {
// returns a user-specific temp dir to avoid conflicts and permission issues
const userId = os.userInfo().username;
const dirName = path.join(os.tmpdir(), 'vscode-download-' + userId);
return getTempDirPathAsync(dirName);
}

protected hasSameOrNewerVersion(id: string, extension: VSXExtensionRaw): string | undefined {
Expand Down

0 comments on commit 84ecd86

Please sign in to comment.