mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-26 05:07:35 +00:00
331 lines
14 KiB
TypeScript
331 lines
14 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { isAbsolute, join, resolve } from 'vs/base/common/path';
|
|
import * as platform from 'vs/base/common/platform';
|
|
import { cwd } from 'vs/base/common/process';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import * as performance from 'vs/base/common/performance';
|
|
import { Event } from 'vs/base/common/event';
|
|
import { IURITransformer, transformOutgoingURIs } from 'vs/base/common/uriIpc';
|
|
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
|
import { ContextKeyDefinedExpr, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyExpression, ContextKeyGreaterEqualsExpr, ContextKeyGreaterExpr, ContextKeyInExpr, ContextKeyNotEqualsExpr, ContextKeyNotExpr, ContextKeyNotInExpr, ContextKeyRegexExpr, ContextKeySmallerEqualsExpr, ContextKeySmallerExpr, IContextKeyExprMapper } from 'vs/platform/contextkey/common/contextkey';
|
|
import { IExtensionGalleryService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
|
|
import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI';
|
|
import { IExtensionsScannerService, toExtensionDescription } from 'vs/platform/extensionManagement/common/extensionsScannerService';
|
|
import { ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
|
import { ILogService } from 'vs/platform/log/common/log';
|
|
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
|
|
import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService';
|
|
import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
|
import { Schemas } from 'vs/base/common/network';
|
|
import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner';
|
|
import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks';
|
|
|
|
export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService {
|
|
|
|
readonly _serviceBrand: undefined;
|
|
|
|
private readonly _whenBuiltinExtensionsReady = Promise.resolve();
|
|
private readonly _whenExtensionsReady = Promise.resolve();
|
|
|
|
constructor(
|
|
private readonly _extensionManagementCLI: ExtensionManagementCLI,
|
|
environmentService: IServerEnvironmentService,
|
|
private readonly _userDataProfilesService: IUserDataProfilesService,
|
|
private readonly _extensionsScannerService: IExtensionsScannerService,
|
|
private readonly _logService: ILogService,
|
|
private readonly _extensionGalleryService: IExtensionGalleryService,
|
|
private readonly _languagePackService: ILanguagePackService
|
|
) {
|
|
const builtinExtensionsToInstall = environmentService.args['install-builtin-extension'];
|
|
if (builtinExtensionsToInstall) {
|
|
_logService.trace('Installing builtin extensions passed via args...');
|
|
const installOptions: InstallOptions = { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] };
|
|
performance.mark('code/server/willInstallBuiltinExtensions');
|
|
this._whenExtensionsReady = this._whenBuiltinExtensionsReady = _extensionManagementCLI.installExtensions([], this._asExtensionIdOrVSIX(builtinExtensionsToInstall), installOptions, !!environmentService.args['force'])
|
|
.then(() => {
|
|
performance.mark('code/server/didInstallBuiltinExtensions');
|
|
_logService.trace('Finished installing builtin extensions');
|
|
}, error => {
|
|
_logService.error(error);
|
|
});
|
|
}
|
|
|
|
const extensionsToInstall = environmentService.args['install-extension'];
|
|
if (extensionsToInstall) {
|
|
_logService.trace('Installing extensions passed via args...');
|
|
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(() => {
|
|
_logService.trace('Finished installing extensions');
|
|
}, error => {
|
|
_logService.error(error);
|
|
});
|
|
}
|
|
}
|
|
|
|
private _asExtensionIdOrVSIX(inputs: string[]): (string | URI)[] {
|
|
return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input);
|
|
}
|
|
|
|
whenExtensionsReady(): Promise<void> {
|
|
return this._whenExtensionsReady;
|
|
}
|
|
|
|
async scanExtensions(
|
|
language?: string,
|
|
profileLocation?: URI,
|
|
workspaceExtensionLocations?: URI[],
|
|
extensionDevelopmentLocations?: URI[],
|
|
languagePackId?: string
|
|
): Promise<IExtensionDescription[]> {
|
|
performance.mark('code/server/willScanExtensions');
|
|
this._logService.trace(`Scanning extensions using UI language: ${language}`);
|
|
|
|
await this._whenBuiltinExtensionsReady;
|
|
|
|
const extensionDevelopmentPaths = extensionDevelopmentLocations ? extensionDevelopmentLocations.filter(url => url.scheme === Schemas.file).map(url => url.fsPath) : undefined;
|
|
profileLocation = profileLocation ?? this._userDataProfilesService.defaultProfile.extensionsResource;
|
|
|
|
const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, workspaceExtensionLocations, extensionDevelopmentPaths, languagePackId);
|
|
|
|
this._logService.trace('Scanned Extensions', extensions);
|
|
this._massageWhenConditions(extensions);
|
|
|
|
performance.mark('code/server/didScanExtensions');
|
|
return extensions;
|
|
}
|
|
|
|
private async _scanExtensions(profileLocation: URI, language: string, workspaceInstalledExtensionLocations: URI[] | undefined, extensionDevelopmentPath: string[] | undefined, languagePackId: string | undefined): Promise<IExtensionDescription[]> {
|
|
await this._ensureLanguagePackIsInstalled(language, languagePackId);
|
|
|
|
const [builtinExtensions, installedExtensions, workspaceInstalledExtensions, developedExtensions] = await Promise.all([
|
|
this._scanBuiltinExtensions(language),
|
|
this._scanInstalledExtensions(profileLocation, language),
|
|
this._scanWorkspaceInstalledExtensions(language, workspaceInstalledExtensionLocations),
|
|
this._scanDevelopedExtensions(language, extensionDevelopmentPath)
|
|
]);
|
|
|
|
return dedupExtensions(builtinExtensions, installedExtensions, workspaceInstalledExtensions, developedExtensions, this._logService);
|
|
}
|
|
|
|
private async _scanDevelopedExtensions(language: string, extensionDevelopmentPaths?: string[]): Promise<IExtensionDescription[]> {
|
|
if (extensionDevelopmentPaths) {
|
|
return (await Promise.all(extensionDevelopmentPaths.map(extensionDevelopmentPath => this._extensionsScannerService.scanOneOrMultipleExtensions(URI.file(resolve(extensionDevelopmentPath)), ExtensionType.User, { language }))))
|
|
.flat()
|
|
.map(e => toExtensionDescription(e, true));
|
|
}
|
|
return [];
|
|
}
|
|
|
|
private async _scanWorkspaceInstalledExtensions(language: string, workspaceInstalledExtensions?: URI[]): Promise<IExtensionDescription[]> {
|
|
const result: IExtensionDescription[] = [];
|
|
if (workspaceInstalledExtensions?.length) {
|
|
const scannedExtensions = await Promise.all(workspaceInstalledExtensions.map(location => this._extensionsScannerService.scanExistingExtension(location, ExtensionType.User, { language })));
|
|
for (const scannedExtension of scannedExtensions) {
|
|
if (scannedExtension) {
|
|
result.push(toExtensionDescription(scannedExtension, false));
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private async _scanBuiltinExtensions(language: string): Promise<IExtensionDescription[]> {
|
|
const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language, useCache: true });
|
|
return scannedExtensions.map(e => toExtensionDescription(e, false));
|
|
}
|
|
|
|
private async _scanInstalledExtensions(profileLocation: URI, language: string): Promise<IExtensionDescription[]> {
|
|
const scannedExtensions = await this._extensionsScannerService.scanUserExtensions({ profileLocation, language, useCache: true });
|
|
return scannedExtensions.map(e => toExtensionDescription(e, false));
|
|
}
|
|
|
|
private async _ensureLanguagePackIsInstalled(language: string, languagePackId: string | undefined): Promise<void> {
|
|
if (
|
|
// No need to install language packs for the default language
|
|
language === platform.LANGUAGE_DEFAULT ||
|
|
// The extension gallery service needs to be available
|
|
!this._extensionGalleryService.isEnabled()
|
|
) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const installed = await this._languagePackService.getInstalledLanguages();
|
|
if (installed.find(p => p.id === language)) {
|
|
this._logService.trace(`Language Pack ${language} is already installed. Skipping language pack installation.`);
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
// We tried to see what is installed but failed. We can try installing anyway.
|
|
this._logService.error(err);
|
|
}
|
|
|
|
if (!languagePackId) {
|
|
this._logService.trace(`No language pack id provided for language ${language}. Skipping language pack installation.`);
|
|
return;
|
|
}
|
|
|
|
this._logService.trace(`Language Pack ${languagePackId} for language ${language} is not installed. It will be installed now.`);
|
|
try {
|
|
await this._extensionManagementCLI.installExtensions([languagePackId], [], { isMachineScoped: true }, true);
|
|
} catch (err) {
|
|
// We tried to install the language pack but failed. We can continue without it thus using the default language.
|
|
this._logService.error(err);
|
|
}
|
|
}
|
|
|
|
private _massageWhenConditions(extensions: IExtensionDescription[]): void {
|
|
// Massage "when" conditions which mention `resourceScheme`
|
|
|
|
interface WhenUser { when?: string }
|
|
|
|
interface LocWhenUser { [loc: string]: WhenUser[] }
|
|
|
|
const _mapResourceSchemeValue = (value: string, isRegex: boolean): string => {
|
|
// console.log(`_mapResourceSchemeValue: ${value}, ${isRegex}`);
|
|
return value.replace(/file/g, 'vscode-remote');
|
|
};
|
|
|
|
const _mapResourceRegExpValue = (value: RegExp): RegExp => {
|
|
let flags = '';
|
|
flags += value.global ? 'g' : '';
|
|
flags += value.ignoreCase ? 'i' : '';
|
|
flags += value.multiline ? 'm' : '';
|
|
return new RegExp(_mapResourceSchemeValue(value.source, true), flags);
|
|
};
|
|
|
|
const _exprKeyMapper = new class implements IContextKeyExprMapper {
|
|
mapDefined(key: string): ContextKeyExpression {
|
|
return ContextKeyDefinedExpr.create(key);
|
|
}
|
|
mapNot(key: string): ContextKeyExpression {
|
|
return ContextKeyNotExpr.create(key);
|
|
}
|
|
mapEquals(key: string, value: any): ContextKeyExpression {
|
|
if (key === 'resourceScheme' && typeof value === 'string') {
|
|
return ContextKeyEqualsExpr.create(key, _mapResourceSchemeValue(value, false));
|
|
} else {
|
|
return ContextKeyEqualsExpr.create(key, value);
|
|
}
|
|
}
|
|
mapNotEquals(key: string, value: any): ContextKeyExpression {
|
|
if (key === 'resourceScheme' && typeof value === 'string') {
|
|
return ContextKeyNotEqualsExpr.create(key, _mapResourceSchemeValue(value, false));
|
|
} else {
|
|
return ContextKeyNotEqualsExpr.create(key, value);
|
|
}
|
|
}
|
|
mapGreater(key: string, value: any): ContextKeyExpression {
|
|
return ContextKeyGreaterExpr.create(key, value);
|
|
}
|
|
mapGreaterEquals(key: string, value: any): ContextKeyExpression {
|
|
return ContextKeyGreaterEqualsExpr.create(key, value);
|
|
}
|
|
mapSmaller(key: string, value: any): ContextKeyExpression {
|
|
return ContextKeySmallerExpr.create(key, value);
|
|
}
|
|
mapSmallerEquals(key: string, value: any): ContextKeyExpression {
|
|
return ContextKeySmallerEqualsExpr.create(key, value);
|
|
}
|
|
mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr {
|
|
if (key === 'resourceScheme' && regexp) {
|
|
return ContextKeyRegexExpr.create(key, _mapResourceRegExpValue(regexp));
|
|
} else {
|
|
return ContextKeyRegexExpr.create(key, regexp);
|
|
}
|
|
}
|
|
mapIn(key: string, valueKey: string): ContextKeyInExpr {
|
|
return ContextKeyInExpr.create(key, valueKey);
|
|
}
|
|
mapNotIn(key: string, valueKey: string): ContextKeyNotInExpr {
|
|
return ContextKeyNotInExpr.create(key, valueKey);
|
|
}
|
|
};
|
|
|
|
const _massageWhenUser = (element: WhenUser) => {
|
|
if (!element || !element.when || !/resourceScheme/.test(element.when)) {
|
|
return;
|
|
}
|
|
|
|
const expr = ContextKeyExpr.deserialize(element.when);
|
|
if (!expr) {
|
|
return;
|
|
}
|
|
|
|
const massaged = expr.map(_exprKeyMapper);
|
|
element.when = massaged.serialize();
|
|
};
|
|
|
|
const _massageWhenUserArr = (elements: WhenUser[] | WhenUser) => {
|
|
if (Array.isArray(elements)) {
|
|
for (const element of elements) {
|
|
_massageWhenUser(element);
|
|
}
|
|
} else {
|
|
_massageWhenUser(elements);
|
|
}
|
|
};
|
|
|
|
const _massageLocWhenUser = (target: LocWhenUser) => {
|
|
for (const loc in target) {
|
|
_massageWhenUserArr(target[loc]);
|
|
}
|
|
};
|
|
|
|
extensions.forEach((extension) => {
|
|
if (extension.contributes) {
|
|
if (extension.contributes.menus) {
|
|
_massageLocWhenUser(<LocWhenUser>extension.contributes.menus);
|
|
}
|
|
if (extension.contributes.keybindings) {
|
|
_massageWhenUserArr(<WhenUser | WhenUser[]>extension.contributes.keybindings);
|
|
}
|
|
if (extension.contributes.views) {
|
|
_massageLocWhenUser(<LocWhenUser>extension.contributes.views);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
export class RemoteExtensionsScannerChannel implements IServerChannel {
|
|
|
|
constructor(private service: RemoteExtensionsScannerService, private getUriTransformer: (requestContext: any) => IURITransformer) { }
|
|
|
|
listen(context: any, event: string): Event<any> {
|
|
throw new Error('Invalid listen');
|
|
}
|
|
|
|
async call(context: any, command: string, args?: any): Promise<any> {
|
|
const uriTransformer = this.getUriTransformer(context);
|
|
switch (command) {
|
|
case 'whenExtensionsReady': return this.service.whenExtensionsReady();
|
|
case 'scanExtensions': {
|
|
const language = args[0];
|
|
const profileLocation = args[1] ? URI.revive(uriTransformer.transformIncoming(args[1])) : undefined;
|
|
const workspaceExtensionLocations = Array.isArray(args[2]) ? args[2].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined;
|
|
const extensionDevelopmentPath = Array.isArray(args[3]) ? args[3].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined;
|
|
const languagePackId: string | undefined = args[4];
|
|
const extensions = await this.service.scanExtensions(
|
|
language,
|
|
profileLocation,
|
|
workspaceExtensionLocations,
|
|
extensionDevelopmentPath,
|
|
languagePackId
|
|
);
|
|
return extensions.map(extension => transformOutgoingURIs(extension, uriTransformer));
|
|
}
|
|
}
|
|
throw new Error('Invalid call');
|
|
}
|
|
}
|