Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attempt to install extensions in workbench that fail to install from remote code-server #238815

Merged
merged 12 commits into from
Feb 11, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,13 @@ export class ExtensionManagementError extends Error {
}
}

export interface InstallExtensionSummary {
failed: {
id: string;
installOptions: InstallOptions;
}[];
}

export type InstallOptions = {
isBuiltin?: boolean;
isWorkspaceScoped?: boolean;
Expand Down
6 changes: 5 additions & 1 deletion src/vs/platform/remote/common/remoteExtensionsScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -13,6 +14,9 @@ export const RemoteExtensionsScannerChannelName = 'remoteExtensionsScanner';
export interface IRemoteExtensionsScannerService {
readonly _serviceBrand: undefined;

whenExtensionsReady(): Promise<void>;
/**
* Returns a promise that resolves to an array of extension identifiers that failed to install
*/
whenExtensionsReady(): Promise<InstallExtensionSummary>;
scanExtensions(): Promise<IExtensionDescription[]>;
}
53 changes: 40 additions & 13 deletions src/vs/server/node/remoteExtensionsScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<InstallExtensionSummary>({ failed: [] });
private readonly _whenExtensionsReady = Promise.resolve<InstallExtensionSummary>({ failed: [] });

constructor(
private readonly _extensionManagementCLI: ExtensionManagementCLI,
Expand All @@ -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) {
Expand All @@ -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 };
});
}
}
Expand All @@ -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<void> {
whenExtensionsReady(): Promise<InstallExtensionSummary> {
return this._whenExtensionsReady;
}

Expand Down Expand Up @@ -308,7 +334,8 @@ export class RemoteExtensionsScannerChannel implements IServerChannel {
async call(context: any, command: string, args?: any): Promise<any> {
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;
Expand Down
2 changes: 1 addition & 1 deletion src/vs/server/node/serverServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -32,10 +33,10 @@ class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService
@ILogService private readonly logService: ILogService,
) { }

whenExtensionsReady(): Promise<void> {
whenExtensionsReady(): Promise<InstallExtensionSummary> {
return this.withChannel(
channel => channel.call('whenExtensionsReady'),
undefined
channel => channel.call<InstallExtensionSummary>('whenExtensionsReady'),
{ failed: [] }
);
}

Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/test/browser/workbenchTestServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -2164,7 +2164,7 @@ export class TestRemoteAgentService implements IRemoteAgentService {

export class TestRemoteExtensionsScannerService implements IRemoteExtensionsScannerService {
declare readonly _serviceBrand: undefined;
async whenExtensionsReady(): Promise<void> { }
async whenExtensionsReady(): Promise<InstallExtensionSummary> { return { failed: [] }; }
scanExtensions(): Promise<IExtensionDescription[]> { throw new Error('Method not implemented.'); }
}

Expand Down
Loading