Skip to content

Commit

Permalink
VSX: Add 'Install Another Version...' Command
Browse files Browse the repository at this point in the history
Supports the ability to install any compatible version of an user-installed extension, provided that the extension is available in the Open VSX Registry.

Co-authored-by: seantan22 <[email protected]>
Co-authored-by: Colin Grant <[email protected]>
  • Loading branch information
colin-grant-work and seantan22 committed Jun 15, 2022
1 parent f6c0689 commit c01e35b
Show file tree
Hide file tree
Showing 17 changed files with 265 additions and 99 deletions.
3 changes: 2 additions & 1 deletion dev-packages/ovsx-client/src/ovsx-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ export class OVSXClient {
return new URL(`${url}${searchUri}`, this.options!.apiUrl).toString();
}

async getExtension(id: string): Promise<VSXExtensionRaw> {
async getExtension(id: string, queryParam?: VSXQueryParam): Promise<VSXExtensionRaw> {
const param: VSXQueryParam = {
...queryParam,
extensionId: id
};
const apiUri = this.buildQueryUri(param);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export class PluginVsCodeFileHandler implements PluginDeployerFileHandler {
fs.copyFile(currentPath, newPath, error => error ? reject(error) : resolve());
});
context.pluginEntry().updatePath(newPath);
context.pluginEntry().storeValue('sourceLocations', [newPath]);
} catch (e) {
console.error(`[${context.pluginEntry().id}]: Failed to copy to user directory. Future sessions may not have access to this plugin.`);
}
Expand Down
14 changes: 10 additions & 4 deletions packages/plugin-ext/src/common/plugin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export interface PluginDeployerResolver {

accept(pluginSourceId: string): boolean;

resolve(pluginResolverContext: PluginDeployerResolverContext): Promise<void>;
resolve(pluginResolverContext: PluginDeployerResolverContext, options?: PluginDeployOptions): Promise<void>;

}

Expand Down Expand Up @@ -853,8 +853,8 @@ export interface PluginDependencies {

export const PluginDeployerHandler = Symbol('PluginDeployerHandler');
export interface PluginDeployerHandler {
deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<void>;
deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<void>;
deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<number | undefined>;
deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<number | undefined>;

getDeployedPluginsById(pluginId: string): DeployedPlugin[];

Expand Down Expand Up @@ -910,6 +910,12 @@ export interface WorkspaceStorageKind {
export type GlobalStorageKind = undefined;
export type PluginStorageKind = GlobalStorageKind | WorkspaceStorageKind;

export interface PluginDeployOptions {
version: string;
/** Instructs the deployer to ignore any existing plugins with different versions */
ignoreOtherVersions?: boolean;
}

/**
* The JSON-RPC workspace interface.
*/
Expand All @@ -922,7 +928,7 @@ export interface PluginServer {
*
* @param type whether a plugin is installed by a system or a user, defaults to a user
*/
deploy(pluginEntry: string, type?: PluginType): Promise<void>;
deploy(pluginEntry: string, type?: PluginType, options?: PluginDeployOptions): Promise<void>;
uninstall(pluginId: PluginIdentifiers.VersionedId): Promise<void>;
undeploy(pluginId: PluginIdentifiers.VersionedId): Promise<void>;

Expand Down
13 changes: 7 additions & 6 deletions packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,15 @@ export class HostedPluginSupport {
for (const versionedId of uninstalledPluginIds) {
const plugin = this.getPlugin(PluginIdentifiers.unversionedFromVersioned(versionedId));
if (plugin && PluginIdentifiers.componentsToVersionedId(plugin.metadata.model) === versionedId && !plugin.metadata.outOfSync) {
didChangeInstallationStatus = true;
plugin.metadata.outOfSync = didChangeInstallationStatus = true;
}
}
for (const contribution of this.contributions.values()) {
if (contribution.plugin.metadata.outOfSync && !uninstalledPluginIds.includes(PluginIdentifiers.componentsToVersionedId(contribution.plugin.metadata.model))) {
contribution.plugin.metadata.outOfSync = false;
didChangeInstallationStatus = true;
}
}
if (newPluginIds.length) {
const plugins = await this.server.getDeployedPlugins({ pluginIds: newPluginIds });
for (const plugin of plugins) {
Expand Down Expand Up @@ -573,11 +578,7 @@ export class HostedPluginSupport {
return;
}
this.activationEvents.add(activationEvent);
const activation: Promise<void>[] = [];
for (const manager of this.managers.values()) {
activation.push(manager.$activateByEvent(activationEvent));
}
await Promise.all(activation);
await Promise.all(Array.from(this.managers.values(), manager => manager.$activateByEvent(activationEvent)));
}

async activateByViewContainer(viewContainerId: string): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
protected readonly uninstallationManager: PluginUninstallationManager;

private readonly deployedLocations = new Map<PluginIdentifiers.VersionedId, Set<string>>();
protected readonly originalLocations = new Map<PluginIdentifiers.VersionedId, string>();
protected readonly sourceLocations = new Map<PluginIdentifiers.VersionedId, Set<string>>();

/**
* Managed plugin metadata backend entries.
Expand Down Expand Up @@ -80,7 +80,7 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
const matches: DeployedPlugin[] = [];
const handle = (plugins: Iterable<DeployedPlugin>): void => {
for (const plugin of plugins) {
if (PluginIdentifiers.componentsToVersionWithId(plugin.metadata.model).version === pluginId) {
if (PluginIdentifiers.componentsToVersionWithId(plugin.metadata.model).id === pluginId) {
matches.push(plugin);
}
}
Expand Down Expand Up @@ -117,28 +117,33 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
}
}

async deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<void> {
async deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<number> {
let successes = 0;
for (const plugin of frontendPlugins) {
await this.deployPlugin(plugin, 'frontend');
if (await this.deployPlugin(plugin, 'frontend')) { successes++; }
}
// resolve on first deploy
this.frontendPluginsMetadataDeferred.resolve(undefined);
return successes;
}

async deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<void> {
async deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<number> {
let successes = 0;
for (const plugin of backendPlugins) {
await this.deployPlugin(plugin, 'backend');
if (await this.deployPlugin(plugin, 'backend')) { successes++; }
}
// rebuild translation config after deployment
this.localizationService.buildTranslationConfig([...this.deployedBackendPlugins.values()]);
// resolve on first deploy
this.backendPluginsMetadataDeferred.resolve(undefined);
return successes;
}

/**
* @throws never! in order to isolate plugin deployment
* @throws never! in order to isolate plugin deployment.
* @returns whether the plugin is deployed after running this function. If the plugin was already installed, will still return `true`.
*/
protected async deployPlugin(entry: PluginDeployerEntry, entryPoint: keyof PluginEntryPoint): Promise<void> {
protected async deployPlugin(entry: PluginDeployerEntry, entryPoint: keyof PluginEntryPoint): Promise<boolean> {
const pluginPath = entry.path();
const deployPlugin = this.stopwatch.start('deployPlugin');
let id;
Expand All @@ -147,23 +152,23 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
const manifest = await this.reader.readPackage(pluginPath);
if (!manifest) {
deployPlugin.error(`Failed to read ${entryPoint} plugin manifest from '${pluginPath}''`);
return;
return success = false;
}

const metadata = this.reader.readMetadata(manifest);
metadata.isUnderDevelopment = entry.getValue('isUnderDevelopment') ?? false;

id = PluginIdentifiers.componentsToVersionedId(metadata.model);

const deployedLocations = this.deployedLocations.get(id) || new Set<string>();
const deployedLocations = this.deployedLocations.get(id) ?? new Set<string>();
deployedLocations.add(entry.rootPath);
this.deployedLocations.set(id, deployedLocations);
this.originalLocations.set(id, entry.originalPath());
this.setSourceLocationsForPlugin(id, entry);

const deployedPlugins = entryPoint === 'backend' ? this.deployedBackendPlugins : this.deployedFrontendPlugins;
if (deployedPlugins.has(id)) {
deployPlugin.debug(`Skipped ${entryPoint} plugin ${metadata.model.name} already deployed`);
return;
return true;
}

const { type } = entry;
Expand All @@ -173,23 +178,25 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
deployedPlugins.set(id, deployed);
deployPlugin.log(`Deployed ${entryPoint} plugin "${id}" from "${metadata.model.entryPoint[entryPoint] || pluginPath}"`);
} catch (e) {
success = false;
deployPlugin.error(`Failed to deploy ${entryPoint} plugin from '${pluginPath}' path`, e);
return success = false;
} finally {
if (success && id) {
this.uninstallationManager.markAsInstalled(id);
this.markAsInstalled(id);
}
}
return success;
}

async uninstallPlugin(pluginId: PluginIdentifiers.VersionedId): Promise<boolean> {
try {
const originalPath = this.originalLocations.get(pluginId);
if (!originalPath) {
const sourceLocations = this.sourceLocations.get(pluginId);
if (!sourceLocations) {
return false;
}
await fs.remove(originalPath);
this.originalLocations.delete(pluginId);
await Promise.all(Array.from(sourceLocations,
location => fs.remove(location).catch(err => console.error(`Failed to remove source for ${pluginId} at ${location}`, err))));
this.sourceLocations.delete(pluginId);
this.uninstallationManager.markAsUninstalled(pluginId);
return true;
} catch (e) {
Expand All @@ -198,6 +205,26 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
}
}

protected markAsInstalled(id: PluginIdentifiers.VersionedId): void {
const metadata = PluginIdentifiers.idAndVersionFromVersionedId(id);
if (metadata) {
const toMarkAsUninstalled: PluginIdentifiers.VersionedId[] = [];
const checkForDifferentVersions = (others: Iterable<PluginIdentifiers.VersionedId>) => {
for (const other of others) {
const otherMetadata = PluginIdentifiers.idAndVersionFromVersionedId(other);
if (metadata.id === otherMetadata?.id && metadata.version !== otherMetadata.version) {
toMarkAsUninstalled.push(other);
}
}
};
checkForDifferentVersions(this.deployedFrontendPlugins.keys());
checkForDifferentVersions(this.deployedBackendPlugins.keys());
this.uninstallationManager.markAsUninstalled(...toMarkAsUninstalled);
this.uninstallationManager.markAsInstalled(id);
toMarkAsUninstalled.forEach(pluginToUninstall => this.uninstallPlugin(pluginToUninstall));
}
}

async undeployPlugin(pluginId: PluginIdentifiers.VersionedId): Promise<boolean> {
this.deployedBackendPlugins.delete(pluginId);
this.deployedFrontendPlugins.delete(pluginId);
Expand All @@ -220,4 +247,14 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {

return true;
}

protected setSourceLocationsForPlugin(id: PluginIdentifiers.VersionedId, entry: PluginDeployerEntry): void {
const knownLocations = this.sourceLocations.get(id) ?? new Set();
const maybeStoredLocations = entry.getValue('sourceLocations');
const storedLocations = Array.isArray(maybeStoredLocations) && maybeStoredLocations.every(location => typeof location === 'string')
? maybeStoredLocations.concat(entry.originalPath())
: [entry.originalPath()];
storedLocations.forEach(location => knownLocations.add(location));
this.sourceLocations.set(id, knownLocations);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class PluginTheiaFileHandler implements PluginDeployerFileHandler {
fs.copyFile(currentPath, newPath, error => error ? reject(error) : resolve());
});
context.pluginEntry().updatePath(newPath);
context.pluginEntry().storeValue('sourceLocations', [newPath]);
} catch (e) {
console.error(`[${context.pluginEntry().id}]: Failed to copy to user directory. Future sessions may not have access to this plugin.`);
}
Expand Down
32 changes: 17 additions & 15 deletions packages/plugin-ext/src/main/node/plugin-deployer-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
PluginDeployerResolver, PluginDeployerFileHandler, PluginDeployerDirectoryHandler,
PluginDeployerEntry, PluginDeployer, PluginDeployerParticipant, PluginDeployerStartContext,
PluginDeployerResolverInit, PluginDeployerFileHandlerContext,
PluginDeployerDirectoryHandlerContext, PluginDeployerEntryType, PluginDeployerHandler, PluginType, UnresolvedPluginEntry, PluginIdentifiers
PluginDeployerDirectoryHandlerContext, PluginDeployerEntryType, PluginDeployerHandler, PluginType, UnresolvedPluginEntry, PluginIdentifiers, PluginDeployOptions
} from '../../common/plugin-protocol';
import { PluginDeployerEntryImpl } from './plugin-deployer-entry-impl';
import {
Expand Down Expand Up @@ -146,15 +146,16 @@ export class PluginDeployerImpl implements PluginDeployer {
}
}

async deploy(plugin: UnresolvedPluginEntry): Promise<void> {
async deploy(plugin: UnresolvedPluginEntry, options?: PluginDeployOptions): Promise<number> {
const deploy = this.measure('deploy');
await this.deployMultipleEntries([plugin]);
deploy.log(`Deploy plugin ${plugin}`);
const numDeployedPlugins = await this.deployMultipleEntries([plugin], options);
deploy.log(`Deploy plugin ${plugin.id}`);
return numDeployedPlugins;
}

protected async deployMultipleEntries(plugins: UnresolvedPluginEntry[]): Promise<void> {
const pluginsToDeploy = await this.resolvePlugins(plugins);
await this.deployPlugins(pluginsToDeploy);
protected async deployMultipleEntries(plugins: UnresolvedPluginEntry[], options?: PluginDeployOptions): Promise<number> {
const pluginsToDeploy = await this.resolvePlugins(plugins, options);
return this.deployPlugins(pluginsToDeploy);
}

/**
Expand All @@ -166,7 +167,7 @@ export class PluginDeployerImpl implements PluginDeployer {
* deployer.deployPlugins(await deployer.resolvePlugins(allPluginEntries));
* ```
*/
async resolvePlugins(plugins: UnresolvedPluginEntry[]): Promise<PluginDeployerEntry[]> {
async resolvePlugins(plugins: UnresolvedPluginEntry[], options?: PluginDeployOptions): Promise<PluginDeployerEntry[]> {
const visited = new Set<string>();
const hasBeenVisited = (id: string) => visited.has(id) || (visited.add(id), false);
const pluginsToDeploy = new Map<PluginIdentifiers.VersionedId, PluginDeployerEntry>();
Expand All @@ -184,7 +185,7 @@ export class PluginDeployerImpl implements PluginDeployer {
}
const type = entry.type ?? PluginType.System;
try {
const pluginDeployerEntries = await this.resolveAndHandle(entry.id, type);
const pluginDeployerEntries = await this.resolveAndHandle(entry.id, type, options);
for (const deployerEntry of pluginDeployerEntries) {
const pluginData = await this.pluginDeployerHandler.getPluginDependencies(deployerEntry);
const versionedId = pluginData && PluginIdentifiers.componentsToVersionedId(pluginData.metadata.model);
Expand Down Expand Up @@ -222,8 +223,8 @@ export class PluginDeployerImpl implements PluginDeployer {
return [...pluginsToDeploy.values()];
}

protected async resolveAndHandle(id: string, type: PluginType): Promise<PluginDeployerEntry[]> {
const entries = await this.resolvePlugin(id, type);
protected async resolveAndHandle(id: string, type: PluginType, options?: PluginDeployOptions): Promise<PluginDeployerEntry[]> {
const entries = await this.resolvePlugin(id, type, options);
await this.applyFileHandlers(entries);
await this.applyDirectoryFileHandlers(entries);
return entries;
Expand Down Expand Up @@ -265,7 +266,7 @@ export class PluginDeployerImpl implements PluginDeployer {
/**
* deploy all plugins that have been accepted
*/
async deployPlugins(pluginsToDeploy: PluginDeployerEntry[]): Promise<any> {
async deployPlugins(pluginsToDeploy: PluginDeployerEntry[]): Promise<number> {
const acceptedPlugins = pluginsToDeploy.filter(pluginDeployerEntry => pluginDeployerEntry.isAccepted());
const acceptedFrontendPlugins = pluginsToDeploy.filter(pluginDeployerEntry => pluginDeployerEntry.isAccepted(PluginDeployerEntryType.FRONTEND));
const acceptedBackendPlugins = pluginsToDeploy.filter(pluginDeployerEntry => pluginDeployerEntry.isAccepted(PluginDeployerEntryType.BACKEND));
Expand All @@ -282,12 +283,13 @@ export class PluginDeployerImpl implements PluginDeployer {
const pluginPaths = acceptedBackendPlugins.map(pluginEntry => pluginEntry.path());
this.logger.debug('local path to deploy on remote instance', pluginPaths);

await Promise.all([
const deployments = await Promise.all([
// start the backend plugins
this.pluginDeployerHandler.deployBackendPlugins(acceptedBackendPlugins),
this.pluginDeployerHandler.deployFrontendPlugins(acceptedFrontendPlugins)
]);
this.onDidDeployEmitter.fire(undefined);
return deployments.reduce<number>((accumulated, current) => accumulated += current ?? 0, 0);
}

/**
Expand Down Expand Up @@ -333,7 +335,7 @@ export class PluginDeployerImpl implements PluginDeployer {
/**
* Check a plugin ID see if there are some resolvers that can handle it. If there is a matching resolver, then we resolve the plugin
*/
public async resolvePlugin(pluginId: string, type: PluginType = PluginType.System): Promise<PluginDeployerEntry[]> {
public async resolvePlugin(pluginId: string, type: PluginType = PluginType.System, options?: PluginDeployOptions): Promise<PluginDeployerEntry[]> {
const pluginDeployerEntries: PluginDeployerEntry[] = [];
const foundPluginResolver = this.pluginResolvers.find(pluginResolver => pluginResolver.accept(pluginId));
// there is a resolver for the input
Expand All @@ -342,7 +344,7 @@ export class PluginDeployerImpl implements PluginDeployer {
// create context object
const context = new PluginDeployerResolverContextImpl(foundPluginResolver, pluginId);

await foundPluginResolver.resolve(context);
await foundPluginResolver.resolve(context, options);

context.getPlugins().forEach(entry => {
entry.type = type;
Expand Down
Loading

0 comments on commit c01e35b

Please sign in to comment.