diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 9e7fc0409fadd..cd1dd853677b1 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -514,6 +514,13 @@ export class ExtensionManagementError extends Error { } } +export interface InstallExtensionSummary { + failed: { + id: string; + installOptions: InstallOptions; + }[]; +} + export type InstallOptions = { isBuiltin?: boolean; isWorkspaceScoped?: boolean; diff --git a/src/vs/platform/remote/common/remoteExtensionsScanner.ts b/src/vs/platform/remote/common/remoteExtensionsScanner.ts index 10959aad50968..cee98538dca08 100644 --- a/src/vs/platform/remote/common/remoteExtensionsScanner.ts +++ b/src/vs/platform/remote/common/remoteExtensionsScanner.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { InstallExtensionSummary } from '../../extensionManagement/common/extensionManagement.js'; import { IExtensionDescription } from '../../extensions/common/extensions.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; @@ -13,6 +14,9 @@ export const RemoteExtensionsScannerChannelName = 'remoteExtensionsScanner'; export interface IRemoteExtensionsScannerService { readonly _serviceBrand: undefined; - whenExtensionsReady(): Promise; + /** + * Returns a promise that resolves to an array of extension identifiers that failed to install + */ + whenExtensionsReady(): Promise; scanExtensions(): Promise; } diff --git a/src/vs/server/node/remoteExtensionsScanner.ts b/src/vs/server/node/remoteExtensionsScanner.ts index 3855e37ce941f..a9712d781f5aa 100644 --- a/src/vs/server/node/remoteExtensionsScanner.ts +++ b/src/vs/server/node/remoteExtensionsScanner.ts @@ -12,7 +12,7 @@ import { Event } from '../../base/common/event.js'; import { IURITransformer, transformOutgoingURIs } from '../../base/common/uriIpc.js'; import { IServerChannel } from '../../base/parts/ipc/common/ipc.js'; import { ContextKeyDefinedExpr, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyExpression, ContextKeyGreaterEqualsExpr, ContextKeyGreaterExpr, ContextKeyInExpr, ContextKeyNotEqualsExpr, ContextKeyNotExpr, ContextKeyNotInExpr, ContextKeyRegexExpr, ContextKeySmallerEqualsExpr, ContextKeySmallerExpr, IContextKeyExprMapper } from '../../platform/contextkey/common/contextkey.js'; -import { IExtensionGalleryService, InstallOptions } from '../../platform/extensionManagement/common/extensionManagement.js'; +import { IExtensionGalleryService, IExtensionManagementService, InstallExtensionSummary, InstallOptions } from '../../platform/extensionManagement/common/extensionManagement.js'; import { ExtensionManagementCLI } from '../../platform/extensionManagement/common/extensionManagementCLI.js'; import { IExtensionsScannerService, toExtensionDescription } from '../../platform/extensionManagement/common/extensionsScannerService.js'; import { ExtensionType, IExtensionDescription } from '../../platform/extensions/common/extensions.js'; @@ -23,13 +23,14 @@ import { dedupExtensions } from '../../workbench/services/extensions/common/exte import { Schemas } from '../../base/common/network.js'; import { IRemoteExtensionsScannerService } from '../../platform/remote/common/remoteExtensionsScanner.js'; import { ILanguagePackService } from '../../platform/languagePacks/common/languagePacks.js'; +import { areSameExtensions } from '../../platform/extensionManagement/common/extensionManagementUtil.js'; export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService { readonly _serviceBrand: undefined; - private readonly _whenBuiltinExtensionsReady = Promise.resolve(); - private readonly _whenExtensionsReady = Promise.resolve(); + private readonly _whenBuiltinExtensionsReady = Promise.resolve({ failed: [] }); + private readonly _whenExtensionsReady = Promise.resolve({ failed: [] }); constructor( private readonly _extensionManagementCLI: ExtensionManagementCLI, @@ -38,7 +39,8 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS private readonly _extensionsScannerService: IExtensionsScannerService, private readonly _logService: ILogService, private readonly _extensionGalleryService: IExtensionGalleryService, - private readonly _languagePackService: ILanguagePackService + private readonly _languagePackService: ILanguagePackService, + private readonly _extensionManagementService: IExtensionManagementService, ) { const builtinExtensionsToInstall = environmentService.args['install-builtin-extension']; if (builtinExtensionsToInstall) { @@ -49,24 +51,48 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS .then(() => { performance.mark('code/server/didInstallBuiltinExtensions'); _logService.trace('Finished installing builtin extensions'); + return { failed: [] }; }, error => { _logService.error(error); + return { failed: [] }; }); } const extensionsToInstall = environmentService.args['install-extension']; if (extensionsToInstall) { _logService.trace('Installing extensions passed via args...'); + const installOptions: InstallOptions = { + isMachineScoped: !!environmentService.args['do-not-sync'], + installPreReleaseVersion: !!environmentService.args['pre-release'], + isApplicationScoped: true // extensions installed during server startup are available to all profiles + }; this._whenExtensionsReady = this._whenBuiltinExtensionsReady - .then(() => _extensionManagementCLI.installExtensions(this._asExtensionIdOrVSIX(extensionsToInstall), [], { - isMachineScoped: !!environmentService.args['do-not-sync'], - installPreReleaseVersion: !!environmentService.args['pre-release'], - isApplicationScoped: true // extensions installed during server startup are available to all profiles - }, !!environmentService.args['force'])) - .then(() => { + .then(() => _extensionManagementCLI.installExtensions(this._asExtensionIdOrVSIX(extensionsToInstall), [], installOptions, !!environmentService.args['force'])) + .then(async () => { _logService.trace('Finished installing extensions'); - }, error => { + return { failed: [] }; + }, async error => { _logService.error(error); + + const failed: { + id: string; + installOptions: InstallOptions; + }[] = []; + const alreadyInstalled = await this._extensionManagementService.getInstalled(ExtensionType.User); + + for (const id of this._asExtensionIdOrVSIX(extensionsToInstall)) { + if (typeof id === 'string') { + if (!alreadyInstalled.some(e => areSameExtensions(e.identifier, { id }))) { + failed.push({ id, installOptions }); + } + } + } + + if (failed.length) { + _logService.info(`Reporting the following extensions as failed to install: ${failed.map(f => f.id).join(', ')}`); + } + + return { failed }; }); } } @@ -75,7 +101,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input); } - whenExtensionsReady(): Promise { + whenExtensionsReady(): Promise { return this._whenExtensionsReady; } @@ -308,7 +334,8 @@ export class RemoteExtensionsScannerChannel implements IServerChannel { async call(context: any, command: string, args?: any): Promise { const uriTransformer = this.getUriTransformer(context); switch (command) { - case 'whenExtensionsReady': return this.service.whenExtensionsReady(); + case 'whenExtensionsReady': return await this.service.whenExtensionsReady(); + case 'scanExtensions': { const language = args[0]; const profileLocation = args[1] ? URI.revive(uriTransformer.transformIncoming(args[1])) : undefined; diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 0824662d2efd1..d66209b4ad639 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -221,7 +221,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(environmentService, logService, ptyHostService, productService, extensionManagementService, configurationService)); - const remoteExtensionsScanner = new RemoteExtensionsScannerService(instantiationService.createInstance(ExtensionManagementCLI, logService), environmentService, userDataProfilesService, extensionsScannerService, logService, extensionGalleryService, languagePackService); + const remoteExtensionsScanner = new RemoteExtensionsScannerService(instantiationService.createInstance(ExtensionManagementCLI, logService), environmentService, userDataProfilesService, extensionsScannerService, logService, extensionGalleryService, languagePackService, extensionManagementService); socketServer.registerChannel(RemoteExtensionsScannerChannelName, new RemoteExtensionsScannerChannel(remoteExtensionsScanner, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority))); const remoteFileSystemChannel = disposables.add(new RemoteAgentFileSystemProviderChannel(logService, environmentService, configurationService)); diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts index 021c893c2b66e..9f793d53124e7 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts @@ -23,6 +23,7 @@ import { DebugExtensionHostAction, DebugExtensionsContribution } from './debugEx import { ExtensionHostProfileService } from './extensionProfileService.js'; import { CleanUpExtensionsFolderAction, OpenExtensionsFolderAction } from './extensionsActions.js'; import { ExtensionsAutoProfiler } from './extensionsAutoProfiler.js'; +import { InstallFailedExtensions } from './installFailedExtensions.js'; import { RemoteExtensionsInitializerContribution } from './remoteExtensionsInit.js'; import { IExtensionHostProfileService, OpenExtensionHostProfileACtion, RuntimeExtensionsEditor, SaveExtensionHostProfileAction, StartExtensionHostProfileAction, StopExtensionHostProfileAction } from './runtimeExtensionsEditor.js'; @@ -72,6 +73,7 @@ workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, Lifecyc workbenchRegistry.registerWorkbenchContribution(ExtensionsAutoProfiler, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(RemoteExtensionsInitializerContribution, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(DebugExtensionsContribution, LifecyclePhase.Restored); +workbenchRegistry.registerWorkbenchContribution(InstallFailedExtensions, LifecyclePhase.Restored); // Register Commands diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/installFailedExtensions.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/installFailedExtensions.ts new file mode 100644 index 0000000000000..3ec70f645d824 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/installFailedExtensions.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IExtensionGalleryService, InstallExtensionInfo } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IRemoteExtensionsScannerService } from '../../../../platform/remote/common/remoteExtensionsScanner.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; +import { IExtensionManagementServerService } from '../../../services/extensionManagement/common/extensionManagement.js'; +export class InstallFailedExtensions extends Disposable implements IWorkbenchContribution { + constructor( + @IRemoteExtensionsScannerService remoteExtensionsScannerService: IRemoteExtensionsScannerService, + @IExtensionGalleryService private readonly _extensionGalleryService: IExtensionGalleryService, + @IExtensionManagementServerService private readonly _extensionManagementServerService: IExtensionManagementServerService, + @ILogService logService: ILogService + ) { + super(); + remoteExtensionsScannerService.whenExtensionsReady() + .then(async ({ failed }) => { + if (failed.length === 0) { + logService.trace('No failed extensions relayed from server'); + return; + } + + if (!this._extensionManagementServerService.remoteExtensionManagementServer) { + logService.error('No remote extension management server available'); + return; + } + + const extensionManagementService = this._extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService; + const galleryExtensions = await this._extensionGalleryService.getExtensions(failed, CancellationToken.None); + // Join back with its installOptions + const installExtensionInfo: InstallExtensionInfo[] = galleryExtensions.map(ext => { + return { + extension: ext, + options: failed.find(f => areSameExtensions(f, ext.identifier))?.installOptions || {} + }; + }); + + await extensionManagementService.installGalleryExtensions(installExtensionInfo); + }).catch(e => { + logService.error(e); + }); + } +} diff --git a/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts b/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts index 0a54b54e48948..fe3e69fb8437f 100644 --- a/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts +++ b/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts @@ -17,6 +17,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { IActiveLanguagePackService } from '../../localization/common/locale.js'; import { IWorkbenchExtensionManagementService } from '../../extensionManagement/common/extensionManagement.js'; import { Mutable } from '../../../../base/common/types.js'; +import { InstallExtensionSummary } from '../../../../platform/extensionManagement/common/extensionManagement.js'; class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService { @@ -32,10 +33,10 @@ class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService @ILogService private readonly logService: ILogService, ) { } - whenExtensionsReady(): Promise { + whenExtensionsReady(): Promise { return this.withChannel( - channel => channel.call('whenExtensionsReady'), - undefined + channel => channel.call('whenExtensionsReady'), + { failed: [] } ); } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 6e422394b6f96..6161e10f542fc 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -161,7 +161,7 @@ import { IUserDataProfile, IUserDataProfilesService, toUserDataProfile, UserData import { UserDataProfileService } from '../../services/userDataProfile/common/userDataProfileService.js'; import { IUserDataProfileService } from '../../services/userDataProfile/common/userDataProfile.js'; import { EnablementState, IExtensionManagementServer, IResourceExtension, IScannedExtension, IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../services/extensionManagement/common/extensionManagement.js'; -import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, IExtensionsControlManifest, IGalleryMetadata, IExtensionManagementParticipant, Metadata, InstallExtensionResult, InstallExtensionInfo, UninstallExtensionInfo } from '../../../platform/extensionManagement/common/extensionManagement.js'; +import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, IExtensionsControlManifest, IGalleryMetadata, IExtensionManagementParticipant, Metadata, InstallExtensionResult, InstallExtensionInfo, UninstallExtensionInfo, InstallExtensionSummary } from '../../../platform/extensionManagement/common/extensionManagement.js'; import { Codicon } from '../../../base/common/codicons.js'; import { IRemoteExtensionsScannerService } from '../../../platform/remote/common/remoteExtensionsScanner.js'; import { IRemoteSocketFactoryService, RemoteSocketFactoryService } from '../../../platform/remote/common/remoteSocketFactoryService.js'; @@ -2164,7 +2164,7 @@ export class TestRemoteAgentService implements IRemoteAgentService { export class TestRemoteExtensionsScannerService implements IRemoteExtensionsScannerService { declare readonly _serviceBrand: undefined; - async whenExtensionsReady(): Promise { } + async whenExtensionsReady(): Promise { return { failed: [] }; } scanExtensions(): Promise { throw new Error('Method not implemented.'); } }