Skip to content

Commit 2c7fff3

Browse files
authored
Add remote.defaultExtensions (microsoft#243400)
* add remote.defaultExtensions * some nice suggestions
1 parent ab67de5 commit 2c7fff3

File tree

4 files changed

+88
-5
lines changed

4 files changed

+88
-5
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
export const REMOTE_DEFAULT_EXTENSIONS = 'remote.defaultExtensions';

src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { DebugExtensionHostAction, DebugExtensionsContribution } from './debugEx
2323
import { ExtensionHostProfileService } from './extensionProfileService.js';
2424
import { CleanUpExtensionsFolderAction, OpenExtensionsFolderAction } from './extensionsActions.js';
2525
import { ExtensionsAutoProfiler } from './extensionsAutoProfiler.js';
26-
import { InstallFailedRemoteExtensionsContribution, RemoteExtensionsInitializerContribution } from './remoteExtensionsInit.js';
26+
import { InstallRemoteExtensionsContribution, RemoteExtensionsInitializerContribution } from './remoteExtensionsInit.js';
2727
import { IExtensionHostProfileService, OpenExtensionHostProfileACtion, RuntimeExtensionsEditor, SaveExtensionHostProfileAction, StartExtensionHostProfileAction, StopExtensionHostProfileAction } from './runtimeExtensionsEditor.js';
2828

2929
// Singletons
@@ -71,7 +71,7 @@ const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(Workbench
7171
workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Restored);
7272
workbenchRegistry.registerWorkbenchContribution(ExtensionsAutoProfiler, LifecyclePhase.Eventually);
7373
workbenchRegistry.registerWorkbenchContribution(RemoteExtensionsInitializerContribution, LifecyclePhase.Restored);
74-
workbenchRegistry.registerWorkbenchContribution(InstallFailedRemoteExtensionsContribution, LifecyclePhase.Restored);
74+
workbenchRegistry.registerWorkbenchContribution(InstallRemoteExtensionsContribution, LifecyclePhase.Restored);
7575
workbenchRegistry.registerWorkbenchContribution(DebugExtensionsContribution, LifecyclePhase.Restored);
7676

7777
// Register Commands

src/vs/workbench/contrib/extensions/electron-sandbox/remoteExtensionsInit.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { CancellationToken } from '../../../../base/common/cancellation.js';
7+
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
78
import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';
89
import { EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT, IExtensionGalleryService, IExtensionManagementService, InstallExtensionInfo } from '../../../../platform/extensionManagement/common/extensionManagement.js';
910
import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
1011
import { IFileService } from '../../../../platform/files/common/files.js';
1112
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
1213
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
1314
import { ILogService } from '../../../../platform/log/common/log.js';
15+
import { IProductService } from '../../../../platform/product/common/productService.js';
16+
import { REMOTE_DEFAULT_EXTENSIONS } from '../../../../platform/remote/common/remote.js';
1417
import { IRemoteAuthorityResolverService } from '../../../../platform/remote/common/remoteAuthorityResolver.js';
1518
import { IRemoteExtensionsScannerService } from '../../../../platform/remote/common/remoteExtensionsScanner.js';
1619
import { IStorageService, IS_NEW_KEY, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
@@ -26,17 +29,74 @@ import { IExtensionManagementServerService } from '../../../services/extensionMa
2629
import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js';
2730
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
2831

29-
export class InstallFailedRemoteExtensionsContribution implements IWorkbenchContribution {
32+
export class InstallRemoteExtensionsContribution implements IWorkbenchContribution {
3033
constructor(
3134
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
3235
@IRemoteExtensionsScannerService private readonly remoteExtensionsScannerService: IRemoteExtensionsScannerService,
3336
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
3437
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
35-
@ILogService private readonly logService: ILogService
38+
@ILogService private readonly logService: ILogService,
39+
@IConfigurationService private readonly configurationService: IConfigurationService,
40+
@IProductService private readonly productService: IProductService,
3641
) {
42+
this.installDefaultRemoteExtensions();
3743
this.installFailedRemoteExtensions();
3844
}
3945

46+
private async installDefaultRemoteExtensions(): Promise<void> {
47+
if (!this.remoteAgentService.getConnection()) {
48+
return;
49+
}
50+
51+
if (!this.extensionManagementServerService.remoteExtensionManagementServer) {
52+
this.logService.error('No remote extension management server available');
53+
return;
54+
}
55+
56+
const settingValue = this.configurationService.getValue<string[]>(REMOTE_DEFAULT_EXTENSIONS);
57+
if (!settingValue?.length) {
58+
return;
59+
}
60+
61+
this.logService.info(`Installing '${settingValue.length}' default remote extensions`);
62+
63+
const preferPrerelease = this.productService.quality !== 'stable';
64+
const galleryExtensions = await this.extensionGalleryService.getExtensions(settingValue.map((id) => ({ id })), CancellationToken.None);
65+
const alreadyInstalledExtensions = await this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.getInstalled();
66+
67+
const prereleaseExtensionInfo: InstallExtensionInfo[] = [];
68+
const extensionInfo: InstallExtensionInfo[] = [];
69+
for (const id of settingValue) {
70+
const alreadyInstalled = alreadyInstalledExtensions.some(e => areSameExtensions(e.identifier, { id }));
71+
if (alreadyInstalled) {
72+
this.logService.trace(`Default remote extension '${id}' is already installed`);
73+
continue;
74+
}
75+
76+
const extension = galleryExtensions.find(e => areSameExtensions(e.identifier, { id }));
77+
if (!extension) {
78+
this.logService.warn(`Default remote extension '${id}' is not found`);
79+
continue;
80+
}
81+
82+
const installPreReleaseVersion = preferPrerelease && extension.hasPreReleaseVersion;
83+
(installPreReleaseVersion ? prereleaseExtensionInfo : extensionInfo).push({
84+
extension, options: { installPreReleaseVersion },
85+
});
86+
}
87+
88+
// Install pre-release extensions first to avoid a situation where:
89+
// An extension without a pre-release (A) is installed first and depends on an extension that has a pre-release version (B)
90+
// If this happens, the extension A may result in the installation of the stable version of B
91+
// A real life example of this is GitHub.copilot and GitHub.copilot-chat
92+
if (prereleaseExtensionInfo.length) {
93+
await Promise.allSettled(prereleaseExtensionInfo.map(e => this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.installFromGallery(e.extension, e.options)));
94+
}
95+
if (extensionInfo.length) {
96+
await Promise.allSettled(extensionInfo.map(e => this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.installFromGallery(e.extension, e.options)));
97+
}
98+
}
99+
40100
private async installFailedRemoteExtensions(): Promise<void> {
41101
if (!this.remoteAgentService.getConnection()) {
42102
return;

src/vs/workbench/contrib/remote/common/remote.contribution.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import { PersistentConnection } from '../../../../platform/remote/common/remoteA
2626
import { IDownloadService } from '../../../../platform/download/common/download.js';
2727
import { DownloadServiceChannel } from '../../../../platform/download/common/downloadIpc.js';
2828
import { RemoteLoggerChannelClient } from '../../../../platform/log/common/logIpc.js';
29+
import { REMOTE_DEFAULT_EXTENSIONS } from '../../../../platform/remote/common/remote.js';
30+
31+
32+
const EXTENSION_IDENTIFIER_PATTERN = '([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$';
2933

3034
export class LabelContribution implements IWorkbenchContribution {
3135

@@ -205,7 +209,7 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
205209
type: 'object',
206210
markdownDescription: localize('remote.extensionKind', "Override the kind of an extension. `ui` extensions are installed and run on the local machine while `workspace` extensions are run on the remote. By overriding an extension's default kind using this setting, you specify if that extension should be installed and enabled locally or remotely."),
207211
patternProperties: {
208-
'([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$': {
212+
[EXTENSION_IDENTIFIER_PATTERN]: {
209213
oneOf: [{ type: 'array', items: extensionKindSchema }, extensionKindSchema],
210214
default: ['ui'],
211215
},
@@ -355,6 +359,19 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
355359
enum: ['localhost', 'allInterfaces'],
356360
default: 'localhost',
357361
description: localize('remote.localPortHost', "Specifies the local host name that will be used for port forwarding.")
362+
},
363+
[REMOTE_DEFAULT_EXTENSIONS]: {
364+
type: 'array',
365+
markdownDescription: localize('remote.defaultExtensions.markdownDescription', 'List of extensions to install automatically on all remotes.'),
366+
default: [
367+
'GitHub.copilot',
368+
'GitHub.copilot-chat'
369+
],
370+
items: {
371+
type: 'string',
372+
pattern: EXTENSION_IDENTIFIER_PATTERN,
373+
patternErrorMessage: localize('remote.defaultExtensions.invalidFormat', 'Extension identifier must be in format "publisher.name".')
374+
},
358375
}
359376
}
360377
});

0 commit comments

Comments
 (0)