From a51f13f232f6fb2742d4a9dd2dadcb9ab009bde5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 4 Apr 2024 18:01:02 +0200 Subject: [PATCH] fix #207813 --- .../abstractExtensionManagementService.ts | 126 ++++++++++++------ .../common/extensionManagementCLI.ts | 2 +- .../node/extensionManagementService.ts | 9 +- .../common/webExtensionManagementService.ts | 2 +- 4 files changed, 93 insertions(+), 46 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 5360015fbfb38..d43b6b3c8d430 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -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'; @@ -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; @@ -204,17 +205,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } protected async installExtensions(extensions: InstallableExtension[]): Promise { - const results: InstallExtensionResult[] = []; - - const installingExtensionsMap = new Map(); + const installExtensionResultsMap = new Map(); + const installingExtensionsMap = new Map(); const alreadyRequestedInstallations: Promise[] = []; - 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 @@ -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; @@ -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 { @@ -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(); @@ -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; } @@ -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 @@ -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); } } @@ -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; } @@ -838,7 +884,7 @@ export abstract class AbstractExtensionTask { return this.cancellablePromise!; } - async run(): Promise { + run(): Promise { if (!this.cancellablePromise) { this.cancellablePromise = createCancelablePromise(token => this.doRun(token)); } diff --git a/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts b/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts index 2dd973658b956..a2040014745c0 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts @@ -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 }) => { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 56e32e7a26986..f3435c9ad9cba 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -803,6 +803,7 @@ abstract class InstallExtensionTask extends AbstractExtensionTask { @@ -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]; @@ -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, @@ -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 { diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index e4f0b773a151d..455087dce4661 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -258,7 +258,7 @@ class InstallExtensionTask extends AbstractExtensionTask 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,