Skip to content

Commit

Permalink
fix #207813 (#209553)
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 authored Apr 4, 2024
1 parent a32d57b commit b4378df
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError,
IProductVersion
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, ExtensionKey, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
Expand All @@ -32,6 +32,7 @@ export type InstallableExtension = { readonly manifest: IExtensionManifest; exte

export type InstallExtensionTaskOptions = InstallOptions & { readonly profileLocation: URI; readonly productVersion: IProductVersion };
export interface IInstallExtensionTask {
readonly manifest: IExtensionManifest;
readonly identifier: IExtensionIdentifier;
readonly source: IGalleryExtension | URI;
readonly operation: InstallOperation;
Expand Down Expand Up @@ -204,17 +205,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
}

protected async installExtensions(extensions: InstallableExtension[]): Promise<InstallExtensionResult[]> {
const results: InstallExtensionResult[] = [];

const installingExtensionsMap = new Map<string, { task: IInstallExtensionTask; manifest: IExtensionManifest }>();
const installExtensionResultsMap = new Map<string, InstallExtensionResult & { profileLocation: URI }>();
const installingExtensionsMap = new Map<string, { task: IInstallExtensionTask; root: IInstallExtensionTask | undefined }>();
const alreadyRequestedInstallations: Promise<any>[] = [];
const successResults: (InstallExtensionResult & { local: ILocalExtension; profileLocation: URI })[] = [];

const getInstallExtensionTaskKey = (extension: IGalleryExtension, profileLocation: URI) => `${ExtensionKey.create(extension).toString()}-${profileLocation.toString()}`;
const createInstallExtensionTask = (manifest: IExtensionManifest, extension: IGalleryExtension | URI, options: InstallExtensionTaskOptions): void => {
const createInstallExtensionTask = (manifest: IExtensionManifest, extension: IGalleryExtension | URI, options: InstallExtensionTaskOptions, root: IInstallExtensionTask | undefined): void => {
const installExtensionTask = this.createInstallExtensionTask(manifest, extension, options);
const key = URI.isUri(extension) ? extension.path : `${extension.identifier.id.toLowerCase()}-${options.profileLocation.toString()}`;
installingExtensionsMap.set(key, { task: installExtensionTask, manifest });
const key = `${getGalleryExtensionId(manifest.publisher, manifest.name)}-${options.profileLocation.toString()}`;
installingExtensionsMap.set(key, { task: installExtensionTask, root });
this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension, profileLocation: options.profileLocation });
this.logService.info('Installing extension:', installExtensionTask.identifier.id);
// only cache gallery extensions tasks
Expand All @@ -240,19 +239,19 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
this.logService.info('Extension is already requested to install', existingInstallExtensionTask.task.identifier.id);
alreadyRequestedInstallations.push(existingInstallExtensionTask.task.waitUntilTaskIsFinished());
} else {
createInstallExtensionTask(manifest, extension, installExtensionTaskOptions);
createInstallExtensionTask(manifest, extension, installExtensionTaskOptions, undefined);
}
}

// collect and start installing all dependencies and pack extensions
await Promise.all([...installingExtensionsMap.values()].map(async ({ task, manifest }) => {
await Promise.all([...installingExtensionsMap.values()].map(async ({ task }) => {
if (task.options.donotIncludePackAndDependencies) {
this.logService.info('Installing the extension without checking dependencies and pack', task.identifier.id);
} else {
try {
const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, manifest, !!task.options.installOnlyNewlyAddedFromExtensionPack, !!task.options.installPreReleaseVersion, task.options.profileLocation, task.options.productVersion);
const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, task.manifest, !!task.options.installOnlyNewlyAddedFromExtensionPack, !!task.options.installPreReleaseVersion, task.options.profileLocation, task.options.productVersion);
const installed = await this.getInstalled(undefined, task.options.profileLocation, task.options.productVersion);
const options: InstallExtensionTaskOptions = { ...task.options, donotIncludePackAndDependencies: true, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } };
const options: InstallExtensionTaskOptions = { ...task.options, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } };
for (const { gallery, manifest } of distinct(allDepsAndPackExtensionsToInstall, ({ gallery }) => gallery.identifier.id)) {
if (installingExtensionsMap.has(`${gallery.identifier.id.toLowerCase()}-${options.profileLocation.toString()}`)) {
continue;
Expand All @@ -277,17 +276,17 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
}));
}
} else if (!installed.some(({ identifier }) => areSameExtensions(identifier, gallery.identifier))) {
createInstallExtensionTask(manifest, gallery, options);
createInstallExtensionTask(manifest, gallery, options, task);
}
}
} catch (error) {
// Installing through VSIX
if (URI.isUri(task.source)) {
// Ignore installing dependencies and packs
if (isNonEmptyArray(manifest.extensionDependencies)) {
if (isNonEmptyArray(task.manifest.extensionDependencies)) {
this.logService.warn(`Cannot install dependencies of extension:`, task.identifier.id, error.message);
}
if (isNonEmptyArray(manifest.extensionPack)) {
if (isNonEmptyArray(task.manifest.extensionPack)) {
this.logService.warn(`Cannot install packed extensions of extension:`, task.identifier.id, error.message);
}
} else {
Expand All @@ -299,7 +298,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
}));

// Install extensions in parallel and wait until all extensions are installed / failed
await this.joinAllSettled([...installingExtensionsMap.values()].map(async ({ task }) => {
await this.joinAllSettled([...installingExtensionsMap.entries()].map(async ([key, { task }]) => {
const startTime = new Date().getTime();
try {
const local = await task.run();
Expand All @@ -320,9 +319,9 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
} catch (error) { /* ignore */ }
}
}

successResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped });
installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped });
} catch (error) {
installExtensionResultsMap.set(key, { error, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: task.options.isApplicationScoped });
this.logService.error('Error while installing the extension', task.identifier.id, getErrorMessage(error));
throw error;
}
Expand All @@ -331,32 +330,69 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
if (alreadyRequestedInstallations.length) {
await this.joinAllSettled(alreadyRequestedInstallations);
}

for (const result of successResults) {
this.logService.info(`Extension installed successfully:`, result.identifier.id);
results.push(result);
}
return results;
return [...installExtensionResultsMap.values()];
} catch (error) {
// rollback installed extensions
if (successResults.length) {
this.logService.info('Rollback: Uninstalling installed extensions', getErrorMessage(error));
await Promise.allSettled(successResults.map(async ({ local, profileLocation }) => {
const getAllDepsAndPacks = (extension: ILocalExtension, profileLocation: URI, allDepsOrPacks: string[]) => {
const depsOrPacks = [];
if (extension.manifest.extensionDependencies?.length) {
depsOrPacks.push(...extension.manifest.extensionDependencies);
}
if (extension.manifest.extensionPack?.length) {
depsOrPacks.push(...extension.manifest.extensionPack);
}
for (const id of depsOrPacks) {
if (allDepsOrPacks.includes(id.toLowerCase())) {
continue;
}
allDepsOrPacks.push(id.toLowerCase());
const installed = installExtensionResultsMap.get(`${id.toLowerCase()}-${profileLocation.toString()}`);
if (installed?.local) {
allDepsOrPacks = getAllDepsAndPacks(installed.local, profileLocation, allDepsOrPacks);
}
}
return allDepsOrPacks;
};
const getErrorResult = (task: IInstallExtensionTask) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: task.options.context, profileLocation: task.profileLocation, error });

const rollbackTasks: IUninstallExtensionTask[] = [];
for (const [key, { task, root }] of installingExtensionsMap) {
const result = installExtensionResultsMap.get(key);
if (!result) {
task.cancel();
installExtensionResultsMap.set(key, getErrorResult(task));
}
// If the extension is installed by a root task and the root task is failed, then uninstall the extension
else if (result.local && root && !installExtensionResultsMap.get(`${root.identifier.id.toLowerCase()}-${task.profileLocation.toString()}`)?.local) {
rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.profileLocation }));
installExtensionResultsMap.set(key, getErrorResult(task));
}
}
for (const [key, { task }] of installingExtensionsMap) {
const result = installExtensionResultsMap.get(key);
if (!result?.local) {
continue;
}
if (task.options.donotIncludePackAndDependencies) {
continue;
}
const depsOrPacks = getAllDepsAndPacks(result.local, task.profileLocation, [result.local.identifier.id.toLowerCase()]).slice(1);
if (depsOrPacks.some(depOrPack => installingExtensionsMap.has(`${depOrPack.toLowerCase()}-${task.profileLocation.toString()}`) && !installExtensionResultsMap.get(`${depOrPack.toLowerCase()}-${task.profileLocation.toString()}`)?.local)) {
rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.profileLocation }));
installExtensionResultsMap.set(key, getErrorResult(task));
}
}

if (rollbackTasks.length) {
await Promise.allSettled(rollbackTasks.map(async rollbackTask => {
try {
await this.createUninstallExtensionTask(local, { versionOnly: true, profileLocation }).run();
this.logService.info('Rollback: Uninstalled extension', local.identifier.id);
await rollbackTask.run();
this.logService.info('Rollback: Uninstalled extension', rollbackTask.extension.identifier.id);
} catch (error) {
this.logService.warn('Rollback: Error while uninstalling extension', local.identifier.id, getErrorMessage(error));
this.logService.warn('Rollback: Error while uninstalling extension', rollbackTask.extension.identifier.id, getErrorMessage(error));
}
}));
}

// cancel all tasks and collect error results
for (const { task } of installingExtensionsMap.values()) {
task.cancel();
results.push({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: task.options.context, profileLocation: task.profileLocation, error });
}

throw error;
} finally {
// Finally, remove all the tasks from the cache
Expand All @@ -365,7 +401,13 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
this.installingExtensions.delete(getInstallExtensionTaskKey(task.source, task.profileLocation));
}
}
if (results.length) {
if (installExtensionResultsMap.size) {
const results = [...installExtensionResultsMap.values()];
for (const result of results) {
if (result.local) {
this.logService.info(`Extension installed successfully:`, result.identifier.id);
}
}
this._onDidInstallExtensions.fire(results);
}
}
Expand Down Expand Up @@ -403,8 +445,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
errors.push(r.reason);
}
}

// If there are errors, throw the error.
if (errors.length) { throw joinErrors(errors); }
if (errors.length) {
throw joinErrors(errors);
}

return results;
}

Expand Down Expand Up @@ -838,7 +884,7 @@ export abstract class AbstractExtensionTask<T> {
return this.cancellablePromise!;
}

async run(): Promise<T> {
run(): Promise<T> {
if (!this.cancellablePromise) {
this.cancellablePromise = createCancelablePromise(token => this.doRun(token));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export class ExtensionManagementCLI {
}
}

const installed = await this.extensionManagementService.getInstalled(ExtensionType.User, installOptions.profileLocation);
const installed = await this.extensionManagementService.getInstalled(undefined, installOptions.profileLocation);

if (installVSIXInfos.length) {
await Promise.all(installVSIXInfos.map(async ({ vsix, installOptions }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,7 @@ abstract class InstallExtensionTask extends AbstractExtensionTask<ILocalExtensio
get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; }

constructor(
readonly manifest: IExtensionManifest,
readonly identifier: IExtensionIdentifier,
readonly source: URI | IGalleryExtension,
readonly options: InstallExtensionTaskOptions,
Expand Down Expand Up @@ -877,7 +878,7 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask {
logService: ILogService,
private readonly telemetryService: ITelemetryService,
) {
super(gallery.identifier, gallery, options, extensionsScanner, uriIdentityService, userDataProfilesService, extensionsScannerService, extensionsProfileScannerService, logService);
super(manifest, gallery.identifier, gallery, options, extensionsScanner, uriIdentityService, userDataProfilesService, extensionsScannerService, extensionsProfileScannerService, logService);
}

protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> {
Expand Down Expand Up @@ -913,7 +914,7 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask {
source: 'gallery',
};

if (existingExtension?.manifest.version === this.gallery.version) {
if (existingExtension && existingExtension.type !== ExtensionType.System && existingExtension.manifest.version === this.gallery.version) {
try {
const local = await this.extensionsScanner.updateMetadata(existingExtension, metadata);
return [local, metadata];
Expand Down Expand Up @@ -988,7 +989,7 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask {
class InstallVSIXTask extends InstallExtensionTask {

constructor(
private readonly manifest: IExtensionManifest,
manifest: IExtensionManifest,
private readonly location: URI,
options: InstallExtensionTaskOptions,
private readonly galleryService: IExtensionGalleryService,
Expand All @@ -999,7 +1000,7 @@ class InstallVSIXTask extends InstallExtensionTask {
extensionsProfileScannerService: IExtensionsProfileScannerService,
logService: ILogService,
) {
super({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, location, options, extensionsScanner, uriIdentityService, userDataProfilesService, extensionsScannerService, extensionsProfileScannerService, logService);
super(manifest, { id: getGalleryExtensionId(manifest.publisher, manifest.name) }, location, options, extensionsScanner, uriIdentityService, userDataProfilesService, extensionsScannerService, extensionsProfileScannerService, logService);
}

protected override async doRun(token: CancellationToken): Promise<ILocalExtension> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ class InstallExtensionTask extends AbstractExtensionTask<ILocalExtension> implem
get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; }

constructor(
manifest: IExtensionManifest,
readonly manifest: IExtensionManifest,
private readonly extension: URI | IGalleryExtension,
readonly options: InstallExtensionTaskOptions,
private readonly webExtensionsScannerService: IWebExtensionsScannerService,
Expand Down

0 comments on commit b4378df

Please sign in to comment.