diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index f681fc5b653..cc9c6d87d95 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -16,9 +16,9 @@ import * as nls from 'vs/nls'; import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation, IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, - InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService + InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo } 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'; @@ -26,6 +26,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; export type ExtensionVerificationStatus = boolean | string; +export type InstallableExtension = { readonly manifest: IExtensionManifest; extension: IGalleryExtension | URI; options: InstallOptions & InstallVSIXOptions }; export type InstallExtensionTaskOptions = InstallOptions & InstallVSIXOptions & { readonly profileLocation: URI }; export interface IInstallExtensionTask { @@ -93,19 +94,54 @@ export abstract class AbstractExtensionManagementService extends Disposable impl async installFromGallery(extension: IGalleryExtension, options: InstallOptions = {}): Promise { try { - if (!this.galleryService.isEnabled()) { - throw new ExtensionManagementError(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"), ExtensionManagementErrorCode.Internal); + const results = await this.installGalleryExtensions([{ extension, options }]); + const result = results.find(({ identifier }) => areSameExtensions(identifier, extension.identifier)); + if (result?.local) { + return result?.local; } - const compatible = await this.checkAndGetCompatibleVersion(extension, !!options.installGivenVersion, !!options.installPreReleaseVersion); - return await this.installExtension(compatible.manifest, compatible.extension, options); + if (result?.error) { + throw result.error; + } + throw toExtensionManagementError(new Error(`Unknown error while installing extension ${extension.identifier.id}`)); } catch (error) { - reportTelemetry(this.telemetryService, 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(extension), error }); - this.logService.error(`Failed to install extension.`, extension.identifier.id); - this.logService.error(error); throw toExtensionManagementError(error); } } + async installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise { + if (!this.galleryService.isEnabled()) { + throw new ExtensionManagementError(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"), ExtensionManagementErrorCode.Internal); + } + + const results: InstallExtensionResult[] = []; + const installableExtensions: InstallableExtension[] = []; + + await Promise.allSettled(extensions.map(async ({ extension, options }) => { + try { + const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion); + installableExtensions.push({ ...compatible, options }); + } catch (error) { + results.push({ identifier: extension.identifier, operation: InstallOperation.Install, source: extension, error }); + } + })); + + if (installableExtensions.length) { + results.push(...await this.installExtensions(installableExtensions)); + } + + for (const result of results) { + if (result.error) { + this.logService.error(`Failed to install extension.`, result.identifier.id); + this.logService.error(result.error); + if (result.source && !URI.isUri(result.source)) { + reportTelemetry(this.telemetryService, 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(result.source), error: result.error }); + } + } + } + + return results; + } + async uninstall(extension: ILocalExtension, options: UninstallOptions = {}): Promise { this.logService.trace('ExtensionManagementService#uninstall', extension.identifier.id); return this.uninstallExtension(extension, options); @@ -126,7 +162,21 @@ export abstract class AbstractExtensionManagementService extends Disposable impl this.participants.push(participant); } - protected async installExtension(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): Promise { + protected async installExtensions(extensions: InstallableExtension[]): Promise { + const results: InstallExtensionResult[] = []; + await Promise.allSettled(extensions.map(async e => { + try { + const result = await this.installExtension(e); + results.push(...result); + } catch (error) { + results.push({ identifier: { id: getGalleryExtensionId(e.manifest.publisher, e.manifest.name) }, operation: InstallOperation.Install, source: e.extension, error }); + } + })); + this._onDidInstallExtensions.fire(results); + return results; + } + + private async installExtension({ manifest, extension, options }: InstallableExtension): Promise { const isApplicationScoped = options.isApplicationScoped || options.isBuiltin || isApplicationScopedExtension(manifest); const installExtensionTaskOptions: InstallExtensionTaskOptions = { @@ -142,7 +192,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const installingExtension = this.installingExtensions.get(getInstallExtensionTaskKey(extension)); if (installingExtension) { this.logService.info('Extensions is already requested to install', extension.identifier.id); - return installingExtension.task.waitUntilTaskIsFinished(); + await installingExtension.task.waitUntilTaskIsFinished(); + return []; } } @@ -272,8 +323,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } installResults.forEach(({ identifier }) => this.logService.info(`Extension installed successfully:`, identifier.id)); - this._onDidInstallExtensions.fire(installResults); - return installResults.filter(({ identifier }) => areSameExtensions(identifier, installExtensionTask.identifier))[0].local; + return installResults; } catch (error) { @@ -299,8 +349,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } } - this._onDidInstallExtensions.fire(allInstallExtensionTasks.map(({ task }) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: installExtensionTaskOptions.context, profileLocation: installExtensionTaskOptions.profileLocation }))); - throw error; + return allInstallExtensionTasks.map(({ task }) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: installExtensionTaskOptions.context, profileLocation: installExtensionTaskOptions.profileLocation, error })); } finally { // Finally, remove all the tasks from the cache for (const { task } of allInstallExtensionTasks) { @@ -678,7 +727,7 @@ export function joinErrors(errorOrErrors: (Error | string) | (Array; readonly profileLocation?: URI; readonly applicationScoped?: boolean; @@ -450,6 +451,8 @@ export interface IExtensionManagementParticipant { postUninstall(local: ILocalExtension, options: UninstallOptions, token: CancellationToken): Promise; } +export type InstallExtensionInfo = { readonly extension: IGalleryExtension; readonly options: InstallOptions }; + export const IExtensionManagementService = createDecorator('extensionManagementService'); export interface IExtensionManagementService { readonly _serviceBrand: undefined; @@ -466,6 +469,7 @@ export interface IExtensionManagementService { install(vsix: URI, options?: InstallVSIXOptions): Promise; canInstall(extension: IGalleryExtension): Promise; installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise; + installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise; installFromLocation(location: URI, profileLocation: URI): Promise; installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise; uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 712e7fc22f9..c1d91312b45 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,7 +9,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI; @@ -128,6 +128,10 @@ export class ExtensionManagementChannel implements IServerChannel { case 'installFromGallery': { return this.service.installFromGallery(args[0], transformIncomingOptions(args[1], uriTransformer)); } + case 'installGalleryExtensions': { + const arg: InstallExtensionInfo[] = args[0]; + return this.service.installGalleryExtensions(arg.map(({ extension, options }) => ({ extension, options: transformIncomingOptions(options, uriTransformer) ?? {} }))); + } case 'uninstall': { return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), transformIncomingOptions(args[1], uriTransformer)); } @@ -250,6 +254,11 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('installFromGallery', [extension, installOptions])).then(local => transformIncomingExtension(local, null)); } + async installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise { + const results = await this.channel.call('installGalleryExtensions', [extensions]); + return results.map(e => ({ ...e, local: e.local ? transformIncomingExtension(e.local, null) : e.local, source: this.isUriComponents(e.source) ? URI.revive(e.source) : e.source, profileLocation: URI.revive(e.profileLocation) })); + } + uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { return Promise.resolve(this.channel.call('uninstall', [extension!, options])); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 840f47e3b22..dff38faf6f5 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -25,7 +25,7 @@ import { extract, ExtractError, IFile, zip } from 'vs/base/node/zip'; import * as nls from 'vs/nls'; import { IDownloadService } from 'vs/platform/download/common/download'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { AbstractExtensionManagementService, AbstractExtensionTask, ExtensionVerificationStatus, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, joinErrors, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { AbstractExtensionManagementService, AbstractExtensionTask, ExtensionVerificationStatus, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, joinErrors, toExtensionManagementError, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOperation, Metadata, InstallVSIXOptions @@ -148,11 +148,19 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi try { const manifest = await getManifest(path.resolve(location.fsPath)); + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode, this.productService.version, this.productService.date)) { - throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", getGalleryExtensionId(manifest.publisher, manifest.name), this.productService.version)); + throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", extensionId, this.productService.version)); } - return await this.installExtension(manifest, location, options); + const result = await this.installExtensions([{ manifest, extension: location, options }]); + if (result[0]?.local) { + return result[0]?.local; + } + if (result[0]?.error) { + throw result[0].error; + } + throw toExtensionManagementError(new Error(`Unknown error while installing extension ${extensionId}`)); } finally { await cleanup(); } diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 5bbed1cfb93..d898288a41e 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -15,7 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; -import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension, DISABLED_EXTENSIONS_STORAGE_PATH, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, EXTENSION_INSTALL_SYNC_CONTEXT } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension, DISABLED_EXTENSIONS_STORAGE_PATH, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -405,7 +405,8 @@ export class LocalExtensionsProvider { async updateLocalExtensions(added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], skippedExtensions: ISyncExtension[], profile: IUserDataProfile): Promise { const syncResourceLogLabel = getSyncResourceLogLabel(SyncResource.Extensions, profile); - const extensionsToInstall: [ISyncExtension, IGalleryExtension][] = []; + const extensionsToInstall: InstallExtensionInfo[] = []; + const syncExtensionsToInstall = new Map(); const removeFromSkipped: IExtensionIdentifier[] = []; const addToSkipped: ISyncExtension[] = []; const installedExtensions = await this.extensionManagementService.getInstalled(undefined, profile.extensionsResource); @@ -473,7 +474,17 @@ export class LocalExtensionsProvider { || (version && installedExtension.manifest.version !== version) // Install if the extension version has changed ) { if (await this.extensionManagementService.canInstall(extension)) { - extensionsToInstall.push([e, extension]); + extensionsToInstall.push({ + extension, options: { + isMachineScoped: false /* set isMachineScoped value to prevent install and sync dialog in web */, + donotIncludePackAndDependencies: true, + installGivenVersion: e.pinned && !!e.version, + installPreReleaseVersion: e.preRelease, + profileLocation: profile.extensionsResource, + context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SYNC_CONTEXT]: true } + } + }); + syncExtensionsToInstall.set(extension.identifier.id.toLowerCase(), e); } else { this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension because it cannot be installed.`, extension.displayName || extension.identifier.id); addToSkipped.push(e); @@ -504,26 +515,22 @@ export class LocalExtensionsProvider { } // 3. Install extensions at the end - for (const [e, extension] of extensionsToInstall) { - try { - this.logService.trace(`${syncResourceLogLabel}: Installing extension...`, extension.identifier.id, extension.version); - await this.extensionManagementService.installFromGallery(extension, { - isMachineScoped: false /* set isMachineScoped value to prevent install and sync dialog in web */, - donotIncludePackAndDependencies: true, - installGivenVersion: e.pinned && !!e.version, - installPreReleaseVersion: e.preRelease, - profileLocation: profile.extensionsResource, - context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SYNC_CONTEXT]: true } - }); - this.logService.info(`${syncResourceLogLabel}: Installed extension.`, extension.identifier.id, extension.version); - removeFromSkipped.push(extension.identifier); - } catch (error) { - addToSkipped.push(e); + const results = await this.extensionManagementService.installGalleryExtensions(extensionsToInstall); + for (const { identifier, local, error, source } of results) { + const gallery = source as IGalleryExtension; + if (local) { + this.logService.info(`${syncResourceLogLabel}: Installed extension.`, identifier.id, gallery.version); + removeFromSkipped.push(identifier); + } else { + const e = syncExtensionsToInstall.get(identifier.id.toLowerCase()); + if (e) { + addToSkipped.push(e); + this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension`, gallery.displayName || gallery.identifier.id); + } if (error instanceof ExtensionManagementError && [ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatiblePreRelease, ExtensionManagementErrorCode.IncompatibleTargetPlatform].includes(error.code)) { - this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension because the compatible extension is not found.`, extension.displayName || extension.identifier.id); - } else { + this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension because the compatible extension is not found.`, gallery.displayName || gallery.identifier.id); + } else if (error) { this.logService.error(error); - this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension`, extension.displayName || extension.identifier.id); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index 7c6c2132599..c08110b911b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -267,17 +267,17 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec return createCancelablePromise(async token => { let accepted = false; const choices: (IPromptChoice | IPromptChoiceWithMenu)[] = []; - const installExtensions = async (isMachineScoped?: boolean) => { + const installExtensions = async (isMachineScoped: boolean) => { this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); onDidInstallRecommendedExtensions(extensions); await Promises.settled([ Promises.settled(extensions.map(extension => this.extensionsWorkbenchService.open(extension, { pinned: true }))), - this.extensionManagementService.installExtensions(extensions.map(e => e.gallery!), { isMachineScoped }) + this.extensionManagementService.installGalleryExtensions(extensions.map(e => ({ extension: e.gallery!, options: { isMachineScoped } }))) ]); }; choices.push({ label: localize('install', "Install"), - run: () => installExtensions(), + run: () => installExtensions(false), menu: this.userDataSyncEnablementService.isEnabled() && this.userDataSyncEnablementService.isResourceEnabled(SyncResource.Extensions) ? [{ label: localize('install and do no sync', "Install (Do not sync)"), run: () => installExtensions(true) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 36f2491569c..84497ffe878 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -1447,7 +1447,7 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView imple async installWorkspaceRecommendations(): Promise { const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); if (installableRecommendations.length) { - await this.extensionManagementService.installExtensions(installableRecommendations.map(i => i.gallery!)); + await this.extensionManagementService.installGalleryExtensions(installableRecommendations.map(i => ({ extension: i.gallery!, options: {} }))); } else { this.notificationService.notify({ severity: Severity.Info, diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 2c15566fffc..362e485948a 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -58,9 +58,7 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten installVSIX(location: URI, manifest: IExtensionManifest, installOptions?: InstallVSIXOptions): Promise; installFromLocation(location: URI): Promise; - installExtensions(extensions: IGalleryExtension[], installOptions?: InstallOptions): Promise; updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension, installOptions?: InstallOptions): Promise; - getExtensionManagementServerToInstall(manifest: IExtensionManifest): IExtensionManagementServer | null; } export const enum EnablementState { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index b29c51da2d5..b21e6f845d4 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,7 +5,7 @@ import { Event, EventMultiplexer } from 'vs/base/common/event'; import { - ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallVSIXOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SYNC_CONTEXT + ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallVSIXOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -296,15 +296,56 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return Promises.settled(servers.map(server => server.extensionManagementService.installFromGallery(gallery, installOptions))).then(([local]) => local); } - async installExtensions(extensions: IGalleryExtension[], installOptions?: InstallOptions): Promise { - if (!installOptions) { - const isMachineScoped = await this.hasToFlagExtensionsMachineScoped(extensions); - installOptions = { isMachineScoped, isBuiltin: false }; - } - return Promises.settled(extensions.map(extension => this.installFromGallery(extension, installOptions))); + async installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise { + const results = new Map(); + + const extensionsByServer = new Map(); + await Promise.all(extensions.map(async ({ extension, options }) => { + try { + const servers = await this.validateAndGetExtensionManagementServersToInstall(extension, options); + if (!options.isMachineScoped && this.isExtensionsSyncEnabled()) { + if (this.extensionManagementServerService.localExtensionManagementServer && !servers.includes(this.extensionManagementServerService.localExtensionManagementServer) && (await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.canInstall(extension))) { + servers.push(this.extensionManagementServerService.localExtensionManagementServer); + } + } + for (const server of servers) { + let exensions = extensionsByServer.get(server); + if (!exensions) { + extensionsByServer.set(server, exensions = []); + } + exensions.push({ extension, options }); + } + } catch (error) { + results.set(extension.identifier.id.toLowerCase(), { identifier: extension.identifier, source: extension, error, operation: InstallOperation.Install }); + } + })); + + await Promise.all([...extensionsByServer.entries()].map(async ([server, extensions]) => { + const serverResults = await server.extensionManagementService.installGalleryExtensions(extensions); + for (const result of serverResults) { + results.set(result.identifier.id.toLowerCase(), result); + } + })); + + return [...results.values()]; } async installFromGallery(gallery: IGalleryExtension, installOptions?: InstallOptions): Promise { + const servers = await this.validateAndGetExtensionManagementServersToInstall(gallery, installOptions); + if (!installOptions || isUndefined(installOptions.isMachineScoped)) { + const isMachineScoped = await this.hasToFlagExtensionsMachineScoped([gallery]); + installOptions = { ...(installOptions || {}), isMachineScoped }; + } + + if (!installOptions.isMachineScoped && this.isExtensionsSyncEnabled()) { + if (this.extensionManagementServerService.localExtensionManagementServer && !servers.includes(this.extensionManagementServerService.localExtensionManagementServer) && (await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.canInstall(gallery))) { + servers.push(this.extensionManagementServerService.localExtensionManagementServer); + } + } + return Promises.settled(servers.map(server => server.extensionManagementService.installFromGallery(gallery, installOptions))).then(([local]) => local); + } + + private async validateAndGetExtensionManagementServersToInstall(gallery: IGalleryExtension, installOptions?: InstallOptions): Promise { const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None); if (!manifest) { @@ -323,32 +364,24 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } } - if (servers.length) { - if (!installOptions || isUndefined(installOptions.isMachineScoped)) { - const isMachineScoped = await this.hasToFlagExtensionsMachineScoped([gallery]); - installOptions = { ...(installOptions || {}), isMachineScoped }; - } - if (!installOptions.isMachineScoped && this.isExtensionsSyncEnabled()) { - if (this.extensionManagementServerService.localExtensionManagementServer && !servers.includes(this.extensionManagementServerService.localExtensionManagementServer) && (await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.canInstall(gallery))) { - servers.push(this.extensionManagementServerService.localExtensionManagementServer); - } - } - if (!installOptions.context?.[EXTENSION_INSTALL_SYNC_CONTEXT]) { - await this.checkForWorkspaceTrust(manifest); - } - if (!installOptions.donotIncludePackAndDependencies) { - await this.checkInstallingExtensionOnWeb(gallery, manifest); - } - return Promises.settled(servers.map(server => server.extensionManagementService.installFromGallery(gallery, installOptions))).then(([local]) => local); + if (!servers.length) { + const error = new Error(localize('cannot be installed', "Cannot install the '{0}' extension because it is not available in this setup.", gallery.displayName || gallery.name)); + error.name = ExtensionManagementErrorCode.Unsupported; + throw error; } - const error = new Error(localize('cannot be installed', "Cannot install the '{0}' extension because it is not available in this setup.", gallery.displayName || gallery.name)); - error.name = ExtensionManagementErrorCode.Unsupported; - return Promise.reject(error); + if (!installOptions?.context?.[EXTENSION_INSTALL_SYNC_CONTEXT]) { + await this.checkForWorkspaceTrust(manifest); + } + + if (!installOptions?.donotIncludePackAndDependencies) { + await this.checkInstallingExtensionOnWeb(gallery, manifest); + } + + return servers; } - getExtensionManagementServerToInstall(manifest: IExtensionManifest): IExtensionManagementServer | null { - + private getExtensionManagementServerToInstall(manifest: IExtensionManifest): IExtensionManagementServer | null { // Only local server if (this.servers.length === 1 && this.extensionManagementServerService.localExtensionManagementServer) { return this.extensionManagementServerService.localExtensionManagementServer; diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index c59003dc9b1..9f0064d709d 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -11,7 +11,7 @@ import { areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionM import { IProfileAwareExtensionManagementService, IScannedExtension, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, toExtensionManagementError, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -105,7 +105,14 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe if (!manifest) { throw new Error(`Cannot find packageJSON from the location ${location.toString()}`); } - return this.installExtension(manifest, location, options); + const result = await this.installExtensions([{ manifest, extension: location, options }]); + if (result[0]?.local) { + return result[0]?.local; + } + if (result[0]?.error) { + throw result[0].error; + } + throw toExtensionManagementError(new Error(`Unknown error while installing extension ${getGalleryExtensionId(manifest.publisher, manifest.name)}`)); } installFromLocation(location: URI, profileLocation: URI): Promise { diff --git a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts index 3585557e837..3940f2a06b7 100644 --- a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts +++ b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts @@ -7,7 +7,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; -import { EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -143,27 +143,34 @@ export class ExtensionsResource implements IProfileResource { } } if (extensionsToInstall.length) { + this.logService.info(`Importing Profile (${profile.name}): Started installing extensions.`); const galleryExtensions = await this.extensionGalleryService.getExtensions(extensionsToInstall.map(e => ({ ...e.identifier, version: e.version, hasPreRelease: e.version ? undefined : e.preRelease })), CancellationToken.None); + const installExtensionInfos: InstallExtensionInfo[] = []; await Promise.all(extensionsToInstall.map(async e => { const extension = galleryExtensions.find(galleryExtension => areSameExtensions(galleryExtension.identifier, e.identifier)); if (!extension) { return; } if (await this.extensionManagementService.canInstall(extension)) { - this.logService.trace(`Importing Profile (${profile.name}): Installing extension...`, extension.identifier.id, extension.version); - await this.extensionManagementService.installFromGallery(extension, { - isMachineScoped: false,/* set isMachineScoped value to prevent install and sync dialog in web */ - donotIncludePackAndDependencies: true, - installGivenVersion: !!e.version, - installPreReleaseVersion: e.preRelease, - profileLocation: profile.extensionsResource, - context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true } + installExtensionInfos.push({ + extension, + options: { + isMachineScoped: false,/* set isMachineScoped value to prevent install and sync dialog in web */ + donotIncludePackAndDependencies: true, + installGivenVersion: !!e.version, + installPreReleaseVersion: e.preRelease, + profileLocation: profile.extensionsResource, + context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true } + } }); - this.logService.info(`Importing Profile (${profile.name}): Installed extension...`, extension.identifier.id, extension.version); } else { this.logService.info(`Importing Profile (${profile.name}): Skipped installing extension because it cannot be installed.`, extension.identifier.id); } })); + if (installExtensionInfos.length) { + await this.extensionManagementService.installGalleryExtensions(installExtensionInfos); + } + this.logService.info(`Importing Profile (${profile.name}): Finished installing extensions.`); } if (extensionsToUninstall.length) { await Promise.all(extensionsToUninstall.map(e => this.extensionManagementService.uninstall(e))); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 68191eae2f6..7fe99b5f2cb 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -161,8 +161,8 @@ import { ILayoutOffsetInfo } from 'vs/platform/layout/browser/layoutService'; import { IUserDataProfile, IUserDataProfilesService, toUserDataProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { EnablementState, IExtensionManagementServer, IScannedExtension, IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { InstallVSIXOptions, ILocalExtension, IGalleryExtension, InstallOptions, IExtensionIdentifier, UninstallOptions, IExtensionsControlManifest, IGalleryMetadata, IExtensionManagementParticipant, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { EnablementState, IScannedExtension, IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { InstallVSIXOptions, ILocalExtension, IGalleryExtension, InstallOptions, IExtensionIdentifier, UninstallOptions, IExtensionsControlManifest, IGalleryMetadata, IExtensionManagementParticipant, Metadata, InstallExtensionResult, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Codicon } from 'vs/base/common/codicons'; import { IHoverOptions, IHoverService, IHoverWidget } from 'vs/workbench/services/hover/browser/hover'; import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; @@ -1993,13 +1993,10 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens installFromLocation(location: URI): Promise { throw new Error('Method not implemented.'); } - installExtensions(extensions: IGalleryExtension[], installOptions?: InstallOptions | undefined): Promise { + installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise { throw new Error('Method not implemented.'); } async updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension, installOptions?: InstallOptions | undefined): Promise { return extension; } - getExtensionManagementServerToInstall(manifest: Readonly): IExtensionManagementServer | null { - throw new Error('Method not implemented.'); - } zip(extension: ILocalExtension): Promise { throw new Error('Method not implemented.'); }