mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-13 11:38:36 -05:00
Merge VS Code 1.31.1 (#4283)
This commit is contained in:
@@ -8,35 +8,20 @@ import Severity from 'vs/base/common/severity';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { ExtensionIdentifier, IExtensionManifest, IExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
|
||||
export interface IExtensionDescription {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
export interface IExtensionDescription extends IExtensionManifest {
|
||||
readonly identifier: ExtensionIdentifier;
|
||||
readonly uuid?: string;
|
||||
readonly displayName?: string;
|
||||
readonly version: string;
|
||||
readonly publisher: string;
|
||||
readonly isBuiltin: boolean;
|
||||
readonly isUnderDevelopment: boolean;
|
||||
readonly extensionLocation: URI;
|
||||
readonly extensionDependencies?: string[];
|
||||
readonly activationEvents?: string[];
|
||||
readonly engines: {
|
||||
vscode: string;
|
||||
// {{SQL CARBON EDIT}}
|
||||
azdata?: string;
|
||||
};
|
||||
readonly main?: string;
|
||||
readonly contributes?: { [point: string]: any; };
|
||||
readonly keywords?: string[];
|
||||
readonly repository?: {
|
||||
url: string;
|
||||
};
|
||||
enableProposedApi?: boolean;
|
||||
}
|
||||
|
||||
export const nullExtensionDescription = Object.freeze(<IExtensionDescription>{
|
||||
id: 'nullExtensionDescription',
|
||||
identifier: new ExtensionIdentifier('nullExtensionDescription'),
|
||||
name: 'Null Extension Description',
|
||||
version: '0.0.0',
|
||||
publisher: 'vscode',
|
||||
@@ -51,7 +36,7 @@ export const IExtensionService = createDecorator<IExtensionService>('extensionSe
|
||||
export interface IMessage {
|
||||
type: Severity;
|
||||
message: string;
|
||||
extensionId: string;
|
||||
extensionId: ExtensionIdentifier;
|
||||
extensionPointId: string;
|
||||
}
|
||||
|
||||
@@ -104,7 +89,7 @@ export interface IExtensionHostProfile {
|
||||
/**
|
||||
* Extension id or one of the four known program states.
|
||||
*/
|
||||
export type ProfileSegmentId = string | 'idle' | 'program' | 'gc' | 'self';
|
||||
export type ProfileSegmentId = string | 'idle' | 'program' | 'gc' | 'self' | null;
|
||||
|
||||
export class ActivationTimes {
|
||||
constructor(
|
||||
@@ -131,7 +116,7 @@ export const ExtensionHostLogFileName = 'exthost';
|
||||
|
||||
export interface IWillActivateEvent {
|
||||
readonly event: string;
|
||||
readonly activation: Thenable<void>;
|
||||
readonly activation: Promise<void>;
|
||||
}
|
||||
|
||||
export interface IResponsiveStateChangeEvent {
|
||||
@@ -156,7 +141,12 @@ export interface IExtensionService extends ICpuProfilerTarget {
|
||||
* Fired when extensions status changes.
|
||||
* The event contains the ids of the extensions that have changed.
|
||||
*/
|
||||
onDidChangeExtensionsStatus: Event<string[]>;
|
||||
onDidChangeExtensionsStatus: Event<ExtensionIdentifier[]>;
|
||||
|
||||
/**
|
||||
* Fired when the available extensions change (i.e. when extensions are added or removed).
|
||||
*/
|
||||
onDidChangeExtensions: Event<void>;
|
||||
|
||||
/**
|
||||
* An event that is fired when activation happens.
|
||||
@@ -172,7 +162,7 @@ export interface IExtensionService extends ICpuProfilerTarget {
|
||||
/**
|
||||
* Send an activation event and activate interested extensions.
|
||||
*/
|
||||
activateByEvent(activationEvent: string): Thenable<void>;
|
||||
activateByEvent(activationEvent: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* An promise that resolves when the installed extensions are registered after
|
||||
@@ -191,6 +181,18 @@ export interface IExtensionService extends ICpuProfilerTarget {
|
||||
*/
|
||||
getExtension(id: string): Promise<IExtensionDescription | undefined>;
|
||||
|
||||
/**
|
||||
* Returns `true` if the given extension can be added. Otherwise `false`.
|
||||
* @param extension An extension
|
||||
*/
|
||||
canAddExtension(extension: IExtensionDescription): boolean;
|
||||
|
||||
/**
|
||||
* Returns `true` if the given extension can be removed. Otherwise `false`.
|
||||
* @param extension An extension
|
||||
*/
|
||||
canRemoveExtension(extension: IExtensionDescription): boolean;
|
||||
|
||||
/**
|
||||
* Read all contributions to an extension point.
|
||||
*/
|
||||
@@ -246,5 +248,14 @@ export function checkProposedApiEnabled(extension: IExtensionDescription): void
|
||||
}
|
||||
|
||||
export function throwProposedApiError(extension: IExtensionDescription): never {
|
||||
throw new Error(`[${extension.id}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${extension.id}`);
|
||||
throw new Error(`[${extension.identifier.value}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${extension.identifier.value}`);
|
||||
}
|
||||
|
||||
export function toExtension(extensionDescription: IExtensionDescription): IExtension {
|
||||
return {
|
||||
type: extensionDescription.isBuiltin ? ExtensionType.System : ExtensionType.User,
|
||||
identifier: { id: getGalleryExtensionId(extensionDescription.publisher, extensionDescription.name), uuid: extensionDescription.uuid },
|
||||
manifest: extensionDescription,
|
||||
location: extensionDescription.extensionLocation,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/co
|
||||
import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IExtensionDescription, IMessage } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
const schemaRegistry = Registry.as<IJSONContributionRegistry>(Extensions.JSONContribution);
|
||||
@@ -35,7 +36,7 @@ export class ExtensionMessageCollector {
|
||||
this._messageHandler({
|
||||
type: type,
|
||||
message: message,
|
||||
extensionId: this._extension.id,
|
||||
extensionId: this._extension.identifier,
|
||||
extensionPointId: this._extensionPointId
|
||||
});
|
||||
}
|
||||
@@ -60,7 +61,7 @@ export interface IExtensionPointUser<T> {
|
||||
}
|
||||
|
||||
export interface IExtensionPointHandler<T> {
|
||||
(extensions: IExtensionPointUser<T>[]): void;
|
||||
(extensions: IExtensionPointUser<T>[], delta: ExtensionPointUserDelta<T>): void;
|
||||
}
|
||||
|
||||
export interface IExtensionPoint<T> {
|
||||
@@ -68,22 +69,60 @@ export interface IExtensionPoint<T> {
|
||||
setHandler(handler: IExtensionPointHandler<T>): void;
|
||||
}
|
||||
|
||||
export class ExtensionPointUserDelta<T> {
|
||||
|
||||
private static _toSet<T>(arr: IExtensionPointUser<T>[]): Set<string> {
|
||||
const result = new Set<string>();
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
result.add(ExtensionIdentifier.toKey(arr[i].description.identifier));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static compute<T>(previous: IExtensionPointUser<T>[] | null, current: IExtensionPointUser<T>[]): ExtensionPointUserDelta<T> {
|
||||
if (!previous || !previous.length) {
|
||||
return new ExtensionPointUserDelta<T>(current, []);
|
||||
}
|
||||
if (!current || !current.length) {
|
||||
return new ExtensionPointUserDelta<T>([], previous);
|
||||
}
|
||||
|
||||
const previousSet = this._toSet(previous);
|
||||
const currentSet = this._toSet(current);
|
||||
|
||||
let added = current.filter(user => !previousSet.has(ExtensionIdentifier.toKey(user.description.identifier)));
|
||||
let removed = previous.filter(user => !currentSet.has(ExtensionIdentifier.toKey(user.description.identifier)));
|
||||
|
||||
return new ExtensionPointUserDelta<T>(added, removed);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly added: IExtensionPointUser<T>[],
|
||||
public readonly removed: IExtensionPointUser<T>[],
|
||||
) { }
|
||||
}
|
||||
|
||||
export class ExtensionPoint<T> implements IExtensionPoint<T> {
|
||||
|
||||
public readonly name: string;
|
||||
private _handler: IExtensionPointHandler<T> | null;
|
||||
private _users: IExtensionPointUser<T>[] | null;
|
||||
private _done: boolean;
|
||||
public readonly isDynamic: boolean;
|
||||
|
||||
constructor(name: string) {
|
||||
private _handler: IExtensionPointHandler<T> | null;
|
||||
private _handlerCalled: boolean;
|
||||
private _users: IExtensionPointUser<T>[] | null;
|
||||
private _delta: ExtensionPointUserDelta<T> | null;
|
||||
|
||||
constructor(name: string, isDynamic: boolean) {
|
||||
this.name = name;
|
||||
this.isDynamic = isDynamic;
|
||||
this._handler = null;
|
||||
this._handlerCalled = false;
|
||||
this._users = null;
|
||||
this._done = false;
|
||||
this._delta = null;
|
||||
}
|
||||
|
||||
setHandler(handler: IExtensionPointHandler<T>): void {
|
||||
if (this._handler !== null || this._done) {
|
||||
if (this._handler !== null) {
|
||||
throw new Error('Handler already set!');
|
||||
}
|
||||
this._handler = handler;
|
||||
@@ -91,27 +130,23 @@ export class ExtensionPoint<T> implements IExtensionPoint<T> {
|
||||
}
|
||||
|
||||
acceptUsers(users: IExtensionPointUser<T>[]): void {
|
||||
if (this._users !== null || this._done) {
|
||||
throw new Error('Users already set!');
|
||||
}
|
||||
this._delta = ExtensionPointUserDelta.compute(this._users, users);
|
||||
this._users = users;
|
||||
this._handle();
|
||||
}
|
||||
|
||||
private _handle(): void {
|
||||
if (this._handler === null || this._users === null) {
|
||||
if (this._handler === null || this._users === null || this._delta === null) {
|
||||
return;
|
||||
}
|
||||
this._done = true;
|
||||
|
||||
let handler = this._handler;
|
||||
this._handler = null;
|
||||
|
||||
let users = this._users;
|
||||
this._users = null;
|
||||
if (this._handlerCalled && !this.isDynamic) {
|
||||
throw new Error('The extension point is not dynamic!');
|
||||
}
|
||||
|
||||
try {
|
||||
handler(users);
|
||||
this._handlerCalled = true;
|
||||
this._handler(this._users, this._delta);
|
||||
} catch (err) {
|
||||
onUnexpectedError(err);
|
||||
}
|
||||
@@ -331,6 +366,13 @@ export const schema = {
|
||||
}
|
||||
};
|
||||
|
||||
export interface IExtensionPointDescriptor {
|
||||
isDynamic?: boolean;
|
||||
extensionPoint: string;
|
||||
deps?: IExtensionPoint<any>[];
|
||||
jsonSchema: IJSONSchema;
|
||||
}
|
||||
|
||||
export class ExtensionsRegistryImpl {
|
||||
|
||||
private _extensionPoints: { [extPoint: string]: ExtensionPoint<any>; };
|
||||
@@ -339,14 +381,14 @@ export class ExtensionsRegistryImpl {
|
||||
this._extensionPoints = {};
|
||||
}
|
||||
|
||||
public registerExtensionPoint<T>(extensionPoint: string, deps: IExtensionPoint<any>[], jsonSchema: IJSONSchema): IExtensionPoint<T> {
|
||||
if (hasOwnProperty.call(this._extensionPoints, extensionPoint)) {
|
||||
throw new Error('Duplicate extension point: ' + extensionPoint);
|
||||
public registerExtensionPoint<T>(desc: IExtensionPointDescriptor): IExtensionPoint<T> {
|
||||
if (hasOwnProperty.call(this._extensionPoints, desc.extensionPoint)) {
|
||||
throw new Error('Duplicate extension point: ' + desc.extensionPoint);
|
||||
}
|
||||
let result = new ExtensionPoint<T>(extensionPoint);
|
||||
this._extensionPoints[extensionPoint] = result;
|
||||
let result = new ExtensionPoint<T>(desc.extensionPoint, desc.isDynamic || false);
|
||||
this._extensionPoints[desc.extensionPoint] = result;
|
||||
|
||||
schema.properties['contributes'].properties[extensionPoint] = jsonSchema;
|
||||
schema.properties['contributes'].properties[desc.extensionPoint] = desc.jsonSchema;
|
||||
schemaRegistry.registerSchema(schemaId, schema);
|
||||
|
||||
return result;
|
||||
@@ -355,6 +397,10 @@ export class ExtensionsRegistryImpl {
|
||||
public getExtensionPoints(): ExtensionPoint<any>[] {
|
||||
return Object.keys(this._extensionPoints).map(point => this._extensionPoints[point]);
|
||||
}
|
||||
|
||||
public getExtensionPointsMap(): { [extPoint: string]: ExtensionPoint<any>; } {
|
||||
return this._extensionPoints;
|
||||
}
|
||||
}
|
||||
|
||||
const PRExtensions = {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { URI } from 'vs/base/common/uri';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER, USER_MANIFEST_CACHE_FILE } from 'vs/platform/extensions/common/extensions';
|
||||
import { BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER, USER_MANIFEST_CACHE_FILE, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import pkg from 'vs/platform/node/package';
|
||||
import product from 'vs/platform/node/product';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
@@ -50,6 +50,7 @@ export class CachedExtensionScanner {
|
||||
public readonly scannedExtensions: Promise<IExtensionDescription[]>;
|
||||
private _scannedExtensionsResolve: (result: IExtensionDescription[]) => void;
|
||||
private _scannedExtensionsReject: (err: any) => void;
|
||||
public readonly translationConfig: Promise<Translations>;
|
||||
|
||||
constructor(
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@@ -61,31 +62,58 @@ export class CachedExtensionScanner {
|
||||
this._scannedExtensionsResolve = resolve;
|
||||
this._scannedExtensionsReject = reject;
|
||||
});
|
||||
this.translationConfig = CachedExtensionScanner._readTranslationConfig();
|
||||
}
|
||||
|
||||
public startScanningExtensions(log: ILog): void {
|
||||
CachedExtensionScanner._scanInstalledExtensions(this._windowService, this._notificationService, this._environmentService, this._extensionEnablementService, log)
|
||||
.then(({ system, user, development }) => {
|
||||
let result: { [extensionId: string]: IExtensionDescription; } = {};
|
||||
system.forEach((systemExtension) => {
|
||||
result[systemExtension.id] = systemExtension;
|
||||
});
|
||||
user.forEach((userExtension) => {
|
||||
if (result.hasOwnProperty(userExtension.id)) {
|
||||
log.warn(userExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[userExtension.id].extensionLocation.fsPath, userExtension.extensionLocation.fsPath));
|
||||
}
|
||||
result[userExtension.id] = userExtension;
|
||||
});
|
||||
development.forEach(developedExtension => {
|
||||
log.info('', nls.localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath));
|
||||
if (result.hasOwnProperty(developedExtension.id)) {
|
||||
log.warn(developedExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[developedExtension.id].extensionLocation.fsPath, developedExtension.extensionLocation.fsPath));
|
||||
}
|
||||
result[developedExtension.id] = developedExtension;
|
||||
});
|
||||
return Object.keys(result).map(name => result[name]);
|
||||
})
|
||||
.then(this._scannedExtensionsResolve, this._scannedExtensionsReject);
|
||||
public async scanSingleExtension(path: string, isBuiltin: boolean, log: ILog): Promise<IExtensionDescription | null> {
|
||||
const translations = await this.translationConfig;
|
||||
|
||||
const version = pkg.version;
|
||||
const commit = product.commit;
|
||||
const devMode = !!process.env['VSCODE_DEV'];
|
||||
const locale = platform.locale;
|
||||
const input = new ExtensionScannerInput(version, commit, locale, devMode, path, isBuiltin, false, translations);
|
||||
return ExtensionScanner.scanSingleExtension(input, log);
|
||||
}
|
||||
|
||||
public async startScanningExtensions(log: ILog): Promise<void> {
|
||||
try {
|
||||
const translations = await this.translationConfig;
|
||||
const { system, user, development } = await CachedExtensionScanner._scanInstalledExtensions(this._windowService, this._notificationService, this._environmentService, this._extensionEnablementService, log, translations);
|
||||
|
||||
let result = new Map<string, IExtensionDescription>();
|
||||
system.forEach((systemExtension) => {
|
||||
const extensionKey = ExtensionIdentifier.toKey(systemExtension.identifier);
|
||||
const extension = result.get(extensionKey);
|
||||
if (extension) {
|
||||
log.warn(systemExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, systemExtension.extensionLocation.fsPath));
|
||||
}
|
||||
result.set(extensionKey, systemExtension);
|
||||
});
|
||||
user.forEach((userExtension) => {
|
||||
const extensionKey = ExtensionIdentifier.toKey(userExtension.identifier);
|
||||
const extension = result.get(extensionKey);
|
||||
if (extension) {
|
||||
log.warn(userExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath));
|
||||
}
|
||||
result.set(extensionKey, userExtension);
|
||||
});
|
||||
development.forEach(developedExtension => {
|
||||
log.info('', nls.localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath));
|
||||
const extensionKey = ExtensionIdentifier.toKey(developedExtension.identifier);
|
||||
const extension = result.get(extensionKey);
|
||||
if (extension) {
|
||||
log.warn(developedExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, developedExtension.extensionLocation.fsPath));
|
||||
}
|
||||
result.set(extensionKey, developedExtension);
|
||||
});
|
||||
let r: IExtensionDescription[] = [];
|
||||
result.forEach((value) => r.push(value));
|
||||
|
||||
this._scannedExtensionsResolve(r);
|
||||
} catch (err) {
|
||||
this._scannedExtensionsReject(err);
|
||||
}
|
||||
}
|
||||
|
||||
private static async _validateExtensionsCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): Promise<void> {
|
||||
@@ -198,86 +226,90 @@ export class CachedExtensionScanner {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _scanInstalledExtensions(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, extensionEnablementService: IExtensionEnablementService, log: ILog): Promise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> {
|
||||
|
||||
const translationConfig: Promise<Translations> = platform.translationsConfigFile
|
||||
? pfs.readFile(platform.translationsConfigFile, 'utf8').then((content) => {
|
||||
try {
|
||||
return JSON.parse(content) as Translations;
|
||||
} catch (err) {
|
||||
return Object.create(null);
|
||||
}
|
||||
}, (err) => {
|
||||
return Object.create(null);
|
||||
})
|
||||
: Promise.resolve(Object.create(null));
|
||||
|
||||
return translationConfig.then((translations) => {
|
||||
const version = pkg.version;
|
||||
const commit = product.commit;
|
||||
const devMode = !!process.env['VSCODE_DEV'];
|
||||
const locale = platform.locale;
|
||||
|
||||
const builtinExtensions = this._scanExtensionsWithCache(
|
||||
windowService,
|
||||
notificationService,
|
||||
environmentService,
|
||||
BUILTIN_MANIFEST_CACHE_FILE,
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations),
|
||||
log
|
||||
);
|
||||
|
||||
let finalBuiltinExtensions: Promise<IExtensionDescription[]> = builtinExtensions;
|
||||
|
||||
if (devMode) {
|
||||
const builtInExtensionsFilePath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'build', 'builtInExtensions.json'));
|
||||
const builtInExtensions = pfs.readFile(builtInExtensionsFilePath, 'utf8')
|
||||
.then<IBuiltInExtension[]>(raw => JSON.parse(raw));
|
||||
|
||||
const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json');
|
||||
const controlFile = pfs.readFile(controlFilePath, 'utf8')
|
||||
.then<IBuiltInExtensionControl>(raw => JSON.parse(raw), () => ({} as any));
|
||||
|
||||
const input = new ExtensionScannerInput(version, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations);
|
||||
const extraBuiltinExtensions = Promise.all([builtInExtensions, controlFile])
|
||||
.then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control))
|
||||
.then(resolver => ExtensionScanner.scanExtensions(input, log, resolver));
|
||||
|
||||
finalBuiltinExtensions = ExtensionScanner.mergeBuiltinExtensions(builtinExtensions, extraBuiltinExtensions);
|
||||
private static async _readTranslationConfig(): Promise<Translations> {
|
||||
if (platform.translationsConfigFile) {
|
||||
try {
|
||||
const content = await pfs.readFile(platform.translationsConfigFile, 'utf8');
|
||||
return JSON.parse(content) as Translations;
|
||||
} catch (err) {
|
||||
// no problemo
|
||||
}
|
||||
}
|
||||
return Object.create(null);
|
||||
}
|
||||
|
||||
const userExtensions = (
|
||||
extensionEnablementService.allUserExtensionsDisabled || !environmentService.extensionsPath
|
||||
? Promise.resolve([])
|
||||
: this._scanExtensionsWithCache(
|
||||
windowService,
|
||||
notificationService,
|
||||
environmentService,
|
||||
USER_MANIFEST_CACHE_FILE,
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, false, translations),
|
||||
log
|
||||
)
|
||||
private static _scanInstalledExtensions(
|
||||
windowService: IWindowService,
|
||||
notificationService: INotificationService,
|
||||
environmentService: IEnvironmentService,
|
||||
extensionEnablementService: IExtensionEnablementService,
|
||||
log: ILog,
|
||||
translations: Translations
|
||||
): Promise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> {
|
||||
|
||||
const version = pkg.version;
|
||||
const commit = product.commit;
|
||||
const devMode = !!process.env['VSCODE_DEV'];
|
||||
const locale = platform.locale;
|
||||
|
||||
const builtinExtensions = this._scanExtensionsWithCache(
|
||||
windowService,
|
||||
notificationService,
|
||||
environmentService,
|
||||
BUILTIN_MANIFEST_CACHE_FILE,
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations),
|
||||
log
|
||||
);
|
||||
|
||||
let finalBuiltinExtensions: Promise<IExtensionDescription[]> = builtinExtensions;
|
||||
|
||||
if (devMode) {
|
||||
const builtInExtensionsFilePath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'build', 'builtInExtensions.json'));
|
||||
const builtInExtensions = pfs.readFile(builtInExtensionsFilePath, 'utf8')
|
||||
.then<IBuiltInExtension[]>(raw => JSON.parse(raw));
|
||||
|
||||
const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json');
|
||||
const controlFile = pfs.readFile(controlFilePath, 'utf8')
|
||||
.then<IBuiltInExtensionControl>(raw => JSON.parse(raw), () => ({} as any));
|
||||
|
||||
const input = new ExtensionScannerInput(version, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations);
|
||||
const extraBuiltinExtensions = Promise.all([builtInExtensions, controlFile])
|
||||
.then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control))
|
||||
.then(resolver => ExtensionScanner.scanExtensions(input, log, resolver));
|
||||
|
||||
finalBuiltinExtensions = ExtensionScanner.mergeBuiltinExtensions(builtinExtensions, extraBuiltinExtensions);
|
||||
}
|
||||
|
||||
const userExtensions = (
|
||||
extensionEnablementService.allUserExtensionsDisabled || !environmentService.extensionsPath
|
||||
? Promise.resolve([])
|
||||
: this._scanExtensionsWithCache(
|
||||
windowService,
|
||||
notificationService,
|
||||
environmentService,
|
||||
USER_MANIFEST_CACHE_FILE,
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, false, translations),
|
||||
log
|
||||
)
|
||||
);
|
||||
|
||||
// Always load developed extensions while extensions development
|
||||
let developedExtensions: Promise<IExtensionDescription[]> = Promise.resolve([]);
|
||||
if (environmentService.isExtensionDevelopment && environmentService.extensionDevelopmentLocationURI && environmentService.extensionDevelopmentLocationURI.scheme === Schemas.file) {
|
||||
developedExtensions = ExtensionScanner.scanOneOrMultipleExtensions(
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, fsPath(environmentService.extensionDevelopmentLocationURI), false, true, translations), log
|
||||
);
|
||||
}
|
||||
|
||||
// Always load developed extensions while extensions development
|
||||
let developedExtensions: Promise<IExtensionDescription[]> = Promise.resolve([]);
|
||||
if (environmentService.isExtensionDevelopment && environmentService.extensionDevelopmentLocationURI && environmentService.extensionDevelopmentLocationURI.scheme === Schemas.file) {
|
||||
developedExtensions = ExtensionScanner.scanOneOrMultipleExtensions(
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, fsPath(environmentService.extensionDevelopmentLocationURI), false, true, translations), log
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all([finalBuiltinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => {
|
||||
const system = extensionDescriptions[0];
|
||||
const user = extensionDescriptions[1];
|
||||
const development = extensionDescriptions[2];
|
||||
return { system, user, development };
|
||||
}).then(null, err => {
|
||||
log.error('', err);
|
||||
return { system: [], user: [], development: [] };
|
||||
});
|
||||
return Promise.all([finalBuiltinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => {
|
||||
const system = extensionDescriptions[0];
|
||||
const user = extensionDescriptions[1];
|
||||
const development = extensionDescriptions[2];
|
||||
return { system, user, development };
|
||||
}).then(undefined, err => {
|
||||
log.error('', err);
|
||||
return { system: [], user: [], development: [] };
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ import { Server, Socket, createServer } from 'net';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Emitter, Event, anyEvent, debounceEvent, fromNodeEventEmitter, mapEvent } from 'vs/base/common/event';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
@@ -21,9 +20,8 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { IRemoteConsoleLog, log, parse } from 'vs/base/node/console';
|
||||
import { findFreePort, randomPort } from 'vs/base/node/ports';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { Protocol, generateRandomPipeName } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { Protocol, generateRandomPipeName, BufferedProtocol } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { IBroadcast, IBroadcastService } from 'vs/platform/broadcast/electron-browser/broadcastService';
|
||||
import { getScopes } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
@@ -34,15 +32,14 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IConfigurationInitData, IInitData } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { IInitData } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/common/extensionHostProtocol';
|
||||
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { ICrashReporterService } from 'vs/workbench/services/crashReporter/electron-browser/crashReporterService';
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
export interface IExtensionHostStarter {
|
||||
readonly onCrashed: Event<[number, string]>;
|
||||
start(): Thenable<IMessagePassingProtocol>;
|
||||
start(): Promise<IMessagePassingProtocol>;
|
||||
getInspectPort(): number;
|
||||
dispose(): void;
|
||||
}
|
||||
@@ -103,7 +100,6 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
@IBroadcastService private readonly _broadcastService: IBroadcastService,
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
@IWorkspaceConfigurationService private readonly _configurationService: IWorkspaceConfigurationService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@ICrashReporterService private readonly _crashReporterService: ICrashReporterService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@@ -176,7 +172,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
|
||||
const opts = {
|
||||
env: objects.mixin(objects.deepClone(process.env), {
|
||||
AMD_ENTRYPOINT: 'vs/workbench/node/extensionHostProcess',
|
||||
AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess',
|
||||
PIPE_LOGGING: 'true',
|
||||
VERBOSE_LOGGING: true,
|
||||
VSCODE_IPC_HOOK_EXTHOST: pipeName,
|
||||
@@ -217,15 +213,15 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
type Output = { data: string, format: string[] };
|
||||
this._extensionHostProcess.stdout.setEncoding('utf8');
|
||||
this._extensionHostProcess.stderr.setEncoding('utf8');
|
||||
const onStdout = fromNodeEventEmitter<string>(this._extensionHostProcess.stdout, 'data');
|
||||
const onStderr = fromNodeEventEmitter<string>(this._extensionHostProcess.stderr, 'data');
|
||||
const onOutput = anyEvent(
|
||||
mapEvent(onStdout, o => ({ data: `%c${o}`, format: [''] })),
|
||||
mapEvent(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }))
|
||||
const onStdout = Event.fromNodeEventEmitter<string>(this._extensionHostProcess.stdout, 'data');
|
||||
const onStderr = Event.fromNodeEventEmitter<string>(this._extensionHostProcess.stderr, 'data');
|
||||
const onOutput = Event.any(
|
||||
Event.map(onStdout, o => ({ data: `%c${o}`, format: [''] })),
|
||||
Event.map(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }))
|
||||
);
|
||||
|
||||
// Debounce all output, so we can render it in the Chrome console as a group
|
||||
const onDebouncedOutput = debounceEvent<Output>(onOutput, (r, o) => {
|
||||
const onDebouncedOutput = Event.debounce<Output>(onOutput, (r, o) => {
|
||||
return r
|
||||
? { data: r.data + o.data, format: [...r.format, ...o.format] }
|
||||
: { data: o.data, format: o.format };
|
||||
@@ -356,7 +352,11 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
this._namedPipeServer.close();
|
||||
this._namedPipeServer = null;
|
||||
this._extensionHostConnection = socket;
|
||||
resolve(new Protocol(this._extensionHostConnection));
|
||||
|
||||
// using a buffered message protocol here because between now
|
||||
// and the first time a `then` executes some messages might be lost
|
||||
// unless we immediately register a listener for `onMessage`.
|
||||
resolve(new BufferedProtocol(new Protocol(this._extensionHostConnection)));
|
||||
});
|
||||
|
||||
}).then((protocol) => {
|
||||
@@ -402,10 +402,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
disposable.dispose();
|
||||
|
||||
// release this promise
|
||||
// using a buffered message protocol here because between now
|
||||
// and the first time a `then` executes some messages might be lost
|
||||
// unless we immediately register a listener for `onMessage`.
|
||||
resolve(new BufferedMessagePassingProtocol(protocol));
|
||||
resolve(protocol);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -420,15 +417,14 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
private _createExtHostInitData(): Promise<IInitData> {
|
||||
return Promise.all([this._telemetryService.getTelemetryInfo(), this._extensions])
|
||||
.then(([telemetryInfo, extensionDescriptions]) => {
|
||||
const configurationData: IConfigurationInitData = { ...this._configurationService.getConfigurationData(), configurationScopes: {} };
|
||||
const workspace = this._contextService.getWorkspace();
|
||||
const r: IInitData = {
|
||||
commit: product.commit,
|
||||
parentPid: process.pid,
|
||||
environment: {
|
||||
isExtensionDevelopmentDebug: this._isExtensionDevDebug,
|
||||
appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : void 0,
|
||||
appSettingsHome: this._environmentService.appSettingsHome ? URI.file(this._environmentService.appSettingsHome) : void 0,
|
||||
appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined,
|
||||
appSettingsHome: this._environmentService.appSettingsHome ? URI.file(this._environmentService.appSettingsHome) : undefined,
|
||||
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
|
||||
extensionTestsPath: this._environmentService.extensionTestsPath,
|
||||
globalStorageHome: URI.file(this._environmentService.globalStorageHome)
|
||||
@@ -439,9 +435,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
id: workspace.id,
|
||||
name: this._labelService.getWorkspaceLabel(workspace)
|
||||
},
|
||||
resolvedExtensions: [],
|
||||
extensions: extensionDescriptions,
|
||||
// Send configurations scopes only in development mode.
|
||||
configuration: !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment ? { ...configurationData, configurationScopes: getScopes() } : configurationData,
|
||||
telemetryInfo,
|
||||
logLevel: this._logService.getLevel(),
|
||||
logsLocation: this._extensionHostLogsLocation,
|
||||
@@ -574,57 +569,3 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will ensure no messages are lost from creation time until the first user of onMessage comes in.
|
||||
*/
|
||||
class BufferedMessagePassingProtocol implements IMessagePassingProtocol {
|
||||
|
||||
private readonly _actual: IMessagePassingProtocol;
|
||||
private _bufferedMessagesListener: IDisposable;
|
||||
private _bufferedMessages: Buffer[];
|
||||
|
||||
constructor(actual: IMessagePassingProtocol) {
|
||||
this._actual = actual;
|
||||
this._bufferedMessages = [];
|
||||
this._bufferedMessagesListener = this._actual.onMessage((buff) => this._bufferedMessages.push(buff));
|
||||
}
|
||||
|
||||
public send(buffer: Buffer): void {
|
||||
this._actual.send(buffer);
|
||||
}
|
||||
|
||||
public onMessage(listener: (e: Buffer) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable {
|
||||
if (!this._bufferedMessages) {
|
||||
// second caller gets nothing
|
||||
return this._actual.onMessage(listener, thisArgs, disposables);
|
||||
}
|
||||
|
||||
// prepare result
|
||||
const result = this._actual.onMessage(listener, thisArgs, disposables);
|
||||
|
||||
// stop listening to buffered messages
|
||||
this._bufferedMessagesListener.dispose();
|
||||
|
||||
// capture buffered messages
|
||||
const bufferedMessages = this._bufferedMessages;
|
||||
this._bufferedMessages = null;
|
||||
|
||||
// it is important to deliver these messages after this call, but before
|
||||
// other messages have a chance to be received (to guarantee in order delivery)
|
||||
// that's why we're using here nextTick and not other types of timeouts
|
||||
process.nextTick(() => {
|
||||
// deliver buffered messages
|
||||
while (bufferedMessages.length > 0) {
|
||||
const msg = bufferedMessages.shift();
|
||||
try {
|
||||
listener.call(thisArgs, msg);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,27 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ExtHostCustomersRegistry } from 'vs/workbench/api/electron-browser/extHostCustomers';
|
||||
import { ExtHostContext, ExtHostExtensionServiceShape, IExtHostContext, MainContext } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { ProfileSession } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ProfileSession, IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IExtensionHostStarter } from 'vs/workbench/services/extensions/electron-browser/extensionHost';
|
||||
import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler';
|
||||
import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier';
|
||||
import { IRPCProtocolLogger, RPCProtocol, RequestInitiator, ResponsiveState } from 'vs/workbench/services/extensions/node/rpcProtocol';
|
||||
import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IUntitledResourceInput } from 'vs/workbench/common/editor';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
|
||||
// Enable to see detailed message communication between window and extension host
|
||||
const LOG_EXTENSION_HOST_COMMUNICATION = false;
|
||||
const LOG_USE_COLORS = true;
|
||||
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(void 0);
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
|
||||
|
||||
export class ExtensionHostProcessManager extends Disposable {
|
||||
|
||||
@@ -42,7 +51,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
/**
|
||||
* winjs believes a proxy is a promise because it has a `then` method, so wrap the result in an object.
|
||||
*/
|
||||
private _extensionHostProcessProxy: Thenable<{ value: ExtHostExtensionServiceShape; }>;
|
||||
private _extensionHostProcessProxy: Promise<{ value: ExtHostExtensionServiceShape; }>;
|
||||
|
||||
constructor(
|
||||
extensionHostProcessWorker: IExtensionHostStarter,
|
||||
@@ -70,6 +79,9 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
);
|
||||
this._extensionHostProcessProxy.then(() => {
|
||||
initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent));
|
||||
this._register(registerLatencyTestProvider({
|
||||
measure: () => this.measure()
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -93,12 +105,63 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
// {{SQL CARBON EDIT}} - Add new getExtensionHostProcessWorker method
|
||||
public getExtenstionHostProcessWorker(): IExtensionHostStarter {
|
||||
return this._extensionHostProcessWorker;
|
||||
}
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
|
||||
private async measure(): Promise<ExtHostLatencyResult> {
|
||||
const latency = await this._measureLatency();
|
||||
const down = await this._measureDown();
|
||||
const up = await this._measureUp();
|
||||
return {
|
||||
remoteAuthority: this._remoteAuthority,
|
||||
latency,
|
||||
down,
|
||||
up
|
||||
};
|
||||
}
|
||||
|
||||
private async _measureLatency(): Promise<number> {
|
||||
const COUNT = 10;
|
||||
|
||||
const { value: proxy } = await this._extensionHostProcessProxy;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
const sw = StopWatch.create(true);
|
||||
await proxy.$test_latency(i);
|
||||
sw.stop();
|
||||
sum += sw.elapsed();
|
||||
}
|
||||
return (sum / COUNT);
|
||||
}
|
||||
|
||||
private static _convert(byteCount: number, elapsedMillis: number): number {
|
||||
return (byteCount * 1000 * 8) / elapsedMillis;
|
||||
}
|
||||
|
||||
private async _measureUp(): Promise<number> {
|
||||
const SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
const { value: proxy } = await this._extensionHostProcessProxy;
|
||||
let b = Buffer.alloc(SIZE, Math.random() % 256);
|
||||
const sw = StopWatch.create(true);
|
||||
await proxy.$test_up(b);
|
||||
sw.stop();
|
||||
return ExtensionHostProcessManager._convert(SIZE, sw.elapsed());
|
||||
}
|
||||
|
||||
private async _measureDown(): Promise<number> {
|
||||
const SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
const { value: proxy } = await this._extensionHostProcessProxy;
|
||||
const sw = StopWatch.create(true);
|
||||
await proxy.$test_down(SIZE);
|
||||
sw.stop();
|
||||
return ExtensionHostProcessManager._convert(SIZE, sw.elapsed());
|
||||
}
|
||||
|
||||
public canProfileExtensionHost(): boolean {
|
||||
return this._extensionHostProcessWorker && Boolean(this._extensionHostProcessWorker.getInspectPort());
|
||||
}
|
||||
@@ -130,8 +193,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
|
||||
// Customers
|
||||
const customers = ExtHostCustomersRegistry.getCustomers();
|
||||
for (let i = 0, len = customers.length; i < len; i++) {
|
||||
const ctor = customers[i];
|
||||
for (const ctor of customers) {
|
||||
const instance = this._instantiationService.createInstance(ctor, extHostContext);
|
||||
this._extensionHostProcessCustomers.push(instance);
|
||||
}
|
||||
@@ -143,7 +205,13 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
return this._extensionHostProcessRPCProtocol.getProxy(ExtHostContext.ExtHostExtensionService);
|
||||
}
|
||||
|
||||
public activateByEvent(activationEvent: string): Thenable<void> {
|
||||
public activate(extension: ExtensionIdentifier, activationEvent: string): Promise<void> {
|
||||
return this._extensionHostProcessProxy.then((proxy) => {
|
||||
return proxy.value.$activate(extension, activationEvent);
|
||||
});
|
||||
}
|
||||
|
||||
public activateByEvent(activationEvent: string): Promise<void> {
|
||||
if (this._extensionHostProcessFinishedActivateEvents[activationEvent] || !this._extensionHostProcessProxy) {
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
@@ -179,13 +247,28 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public resolveAuthority(remoteAuthority: string): Thenable<ResolvedAuthority> {
|
||||
public resolveAuthority(remoteAuthority: string): Promise<ResolvedAuthority> {
|
||||
const authorityPlusIndex = remoteAuthority.indexOf('+');
|
||||
if (authorityPlusIndex === -1) {
|
||||
// This authority does not need to be resolved, simply parse the port number
|
||||
const pieces = remoteAuthority.split(':');
|
||||
return Promise.resolve({
|
||||
authority: remoteAuthority,
|
||||
host: pieces[0],
|
||||
port: parseInt(pieces[1], 10),
|
||||
syncExtensions: false
|
||||
});
|
||||
}
|
||||
return this._extensionHostProcessProxy.then(proxy => proxy.value.$resolveAuthority(remoteAuthority));
|
||||
}
|
||||
|
||||
public start(enabledExtensionIds: string[]): Thenable<void> {
|
||||
public start(enabledExtensionIds: ExtensionIdentifier[]): Promise<void> {
|
||||
return this._extensionHostProcessProxy.then(proxy => proxy.value.$startExtensionHost(enabledExtensionIds));
|
||||
}
|
||||
|
||||
public deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void> {
|
||||
return this._extensionHostProcessProxy.then(proxy => proxy.value.$deltaExtensions(toAdd, toRemove));
|
||||
}
|
||||
}
|
||||
|
||||
const colorTables = [
|
||||
@@ -230,7 +313,7 @@ class RPCLogger implements IRPCProtocolLogger {
|
||||
} else {
|
||||
args.push(data);
|
||||
}
|
||||
console.log.apply(console, args);
|
||||
console.log.apply(console, args as [string, ...string[]]);
|
||||
}
|
||||
|
||||
logIncoming(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void {
|
||||
@@ -243,3 +326,68 @@ class RPCLogger implements IRPCProtocolLogger {
|
||||
this._log('Win \u2192 Ext', this._totalOutgoing, msgLength, req, initiator, str, data);
|
||||
}
|
||||
}
|
||||
|
||||
interface ExtHostLatencyResult {
|
||||
remoteAuthority: string;
|
||||
up: number;
|
||||
down: number;
|
||||
latency: number;
|
||||
}
|
||||
|
||||
interface ExtHostLatencyProvider {
|
||||
measure(): Promise<ExtHostLatencyResult>;
|
||||
}
|
||||
|
||||
let providers: ExtHostLatencyProvider[] = [];
|
||||
function registerLatencyTestProvider(provider: ExtHostLatencyProvider): IDisposable {
|
||||
providers.push(provider);
|
||||
return {
|
||||
dispose: () => {
|
||||
for (let i = 0; i < providers.length; i++) {
|
||||
if (providers[i] === provider) {
|
||||
providers.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getLatencyTestProviders(): ExtHostLatencyProvider[] {
|
||||
return providers.slice(0);
|
||||
}
|
||||
|
||||
export class MeasureExtHostLatencyAction extends Action {
|
||||
public static readonly ID = 'editor.action.measureExtHostLatency';
|
||||
public static readonly LABEL = nls.localize('measureExtHostLatency', "Measure Extension Host Latency");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IEditorService private readonly _editorService: IEditorService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public async run(): Promise<any> {
|
||||
const measurements = await Promise.all(getLatencyTestProviders().map(provider => provider.measure()));
|
||||
this._editorService.openEditor({ contents: measurements.map(MeasureExtHostLatencyAction._print).join('\n\n'), options: { pinned: true } } as IUntitledResourceInput);
|
||||
}
|
||||
|
||||
private static _print(m: ExtHostLatencyResult): string {
|
||||
return `${m.remoteAuthority ? `Authority: ${m.remoteAuthority}\n` : ``}Roundtrip latency: ${m.latency.toFixed(3)}ms\nUp: ${MeasureExtHostLatencyAction._printSpeed(m.up)}\nDown: ${MeasureExtHostLatencyAction._printSpeed(m.down)}\n`;
|
||||
}
|
||||
|
||||
private static _printSpeed(n: number): string {
|
||||
if (n <= 1024) {
|
||||
return `${n} bps`;
|
||||
}
|
||||
if (n < 1024 * 1024) {
|
||||
return `${(n / 1024).toFixed(1)} kbps`;
|
||||
}
|
||||
return `${(n / 1024 / 1024).toFixed(1)} Mbps`;
|
||||
}
|
||||
}
|
||||
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(MeasureExtHostLatencyAction, MeasureExtHostLatencyAction.ID, MeasureExtHostLatencyAction.LABEL), 'Developer: Measure Extension Host Latency', nls.localize('developer', "Developer"));
|
||||
|
||||
@@ -15,7 +15,7 @@ export class ExtensionHostProfiler {
|
||||
|
||||
public async start(): Promise<ProfileSession> {
|
||||
const profiler = await import('v8-inspect-profiler');
|
||||
const session = await profiler.startProfiling({ port: this._port });
|
||||
const session = await profiler.startProfiling({ port: this._port, checkForPaused: true });
|
||||
return {
|
||||
stop: async () => {
|
||||
const profile = await session.stop();
|
||||
@@ -56,26 +56,29 @@ export class ExtensionHostProfiler {
|
||||
} else if (segmentId === 'self' && node.callFrame.url) {
|
||||
let extension = searchTree.findSubstr(node.callFrame.url);
|
||||
if (extension) {
|
||||
segmentId = extension.id;
|
||||
segmentId = extension.identifier.value;
|
||||
}
|
||||
}
|
||||
idsToSegmentId.set(node.id, segmentId);
|
||||
|
||||
if (node.children) {
|
||||
for (let child of node.children) {
|
||||
visit(idsToNodes.get(child), segmentId);
|
||||
for (const child of node.children) {
|
||||
const childNode = idsToNodes.get(child);
|
||||
if (childNode) {
|
||||
visit(childNode, segmentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
visit(nodes[0], null);
|
||||
|
||||
let samples = profile.samples;
|
||||
let timeDeltas = profile.timeDeltas;
|
||||
const samples = profile.samples || [];
|
||||
let timeDeltas = profile.timeDeltas || [];
|
||||
let distilledDeltas: number[] = [];
|
||||
let distilledIds: ProfileSegmentId[] = [];
|
||||
|
||||
let currSegmentTime = 0;
|
||||
let currSegmentId: string = void 0;
|
||||
let currSegmentId: string | undefined;
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
let id = samples[i];
|
||||
let segmentId = idsToSegmentId.get(id);
|
||||
@@ -84,7 +87,7 @@ export class ExtensionHostProfiler {
|
||||
distilledIds.push(currSegmentId);
|
||||
distilledDeltas.push(currSegmentTime);
|
||||
}
|
||||
currSegmentId = segmentId;
|
||||
currSegmentId = segmentId || undefined;
|
||||
currSegmentTime = 0;
|
||||
}
|
||||
currSegmentTime += timeDeltas[i];
|
||||
@@ -93,9 +96,6 @@ export class ExtensionHostProfiler {
|
||||
distilledIds.push(currSegmentId);
|
||||
distilledDeltas.push(currSegmentTime);
|
||||
}
|
||||
idsToNodes = null;
|
||||
idsToSegmentId = null;
|
||||
searchTree = null;
|
||||
|
||||
return {
|
||||
startTime: profile.startTime,
|
||||
|
||||
@@ -13,8 +13,8 @@ import * as perf from 'vs/base/common/performance';
|
||||
import { isEqualOrParent } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { EnablementState, IExtensionEnablementService, IExtensionIdentifier, IExtensionManagementService, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { BetterMergeId, areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { EnablementState, IExtensionEnablementService, IExtensionIdentifier, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { BetterMergeId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import pkg from 'vs/platform/node/package';
|
||||
@@ -22,19 +22,40 @@ import product from 'vs/platform/node/product';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
|
||||
import { ActivationTimes, ExtensionPointContribution, IExtensionDescription, IExtensionService, IExtensionsStatus, IMessage, ProfileSession, IWillActivateEvent, IResponsiveStateChangeEvent } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ActivationTimes, ExtensionPointContribution, IExtensionDescription, IExtensionService, IExtensionsStatus, IMessage, ProfileSession, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser, schema } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { ExtensionHostProcessWorker } from 'vs/workbench/services/extensions/electron-browser/extensionHost';
|
||||
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry';
|
||||
import { ResponsiveState } from 'vs/workbench/services/extensions/node/rpcProtocol';
|
||||
import { CachedExtensionScanner, Logger } from 'vs/workbench/services/extensions/electron-browser/cachedExtensionScanner';
|
||||
import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/electron-browser/extensionHostProcessManager';
|
||||
import { ExtensionIdentifier, IExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(void 0);
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
|
||||
|
||||
schema.properties.engines.properties.vscode.default = `^${pkg.version}`;
|
||||
|
||||
let productAllowProposedApi: Set<string> = null;
|
||||
function allowProposedApiFromProduct(id: ExtensionIdentifier): boolean {
|
||||
// create set if needed
|
||||
if (productAllowProposedApi === null) {
|
||||
productAllowProposedApi = new Set<string>();
|
||||
if (isNonEmptyArray(product.extensionAllowedProposedApi)) {
|
||||
product.extensionAllowedProposedApi.forEach((id) => productAllowProposedApi.add(ExtensionIdentifier.toKey(id)));
|
||||
}
|
||||
}
|
||||
return productAllowProposedApi.has(ExtensionIdentifier.toKey(id));
|
||||
}
|
||||
|
||||
class DeltaExtensionsQueueItem {
|
||||
constructor(
|
||||
public readonly toAdd: IExtension[],
|
||||
public readonly toRemove: string[]
|
||||
) { }
|
||||
}
|
||||
|
||||
export class ExtensionService extends Disposable implements IExtensionService {
|
||||
|
||||
public _serviceBrand: any;
|
||||
@@ -43,15 +64,19 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
private _registry: ExtensionDescriptionRegistry;
|
||||
private readonly _installedExtensionsReady: Barrier;
|
||||
private readonly _isDev: boolean;
|
||||
private readonly _extensionsMessages: { [id: string]: IMessage[] };
|
||||
private readonly _extensionsMessages: Map<string, IMessage[]>;
|
||||
private _allRequestedActivateEvents: { [activationEvent: string]: boolean; };
|
||||
private readonly _extensionScanner: CachedExtensionScanner;
|
||||
private _deltaExtensionsQueue: DeltaExtensionsQueueItem[];
|
||||
|
||||
private readonly _onDidRegisterExtensions: Emitter<void> = this._register(new Emitter<void>({ leakWarningThreshold: 500 }));
|
||||
private readonly _onDidRegisterExtensions: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onDidRegisterExtensions = this._onDidRegisterExtensions.event;
|
||||
|
||||
private readonly _onDidChangeExtensionsStatus: Emitter<string[]> = this._register(new Emitter<string[]>());
|
||||
public readonly onDidChangeExtensionsStatus: Event<string[]> = this._onDidChangeExtensionsStatus.event;
|
||||
private readonly _onDidChangeExtensionsStatus: Emitter<ExtensionIdentifier[]> = this._register(new Emitter<ExtensionIdentifier[]>());
|
||||
public readonly onDidChangeExtensionsStatus: Event<ExtensionIdentifier[]> = this._onDidChangeExtensionsStatus.event;
|
||||
|
||||
private readonly _onDidChangeExtensions: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeExtensions: Event<void> = this._onDidChangeExtensions.event;
|
||||
|
||||
private readonly _onWillActivateByEvent = this._register(new Emitter<IWillActivateEvent>());
|
||||
public readonly onWillActivateByEvent: Event<IWillActivateEvent> = this._onWillActivateByEvent.event;
|
||||
@@ -61,8 +86,9 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
|
||||
// --- Members used per extension host process
|
||||
private _extensionHostProcessManagers: ExtensionHostProcessManager[];
|
||||
private _extensionHostProcessActivationTimes: { [id: string]: ActivationTimes; };
|
||||
private _extensionHostExtensionRuntimeErrors: { [id: string]: Error[]; };
|
||||
private _extensionHostActiveExtensions: Map<string, ExtensionIdentifier>;
|
||||
private _extensionHostProcessActivationTimes: Map<string, ActivationTimes>;
|
||||
private _extensionHostExtensionRuntimeErrors: Map<string, Error[]>;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@@ -70,22 +96,24 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@IExtensionEnablementService private readonly _extensionEnablementService: IExtensionEnablementService,
|
||||
@IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService,
|
||||
@IWindowService private readonly _windowService: IWindowService,
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
|
||||
@IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService
|
||||
) {
|
||||
super();
|
||||
this._extensionHostLogsLocation = URI.file(path.posix.join(this._environmentService.logsPath, `exthost${this._windowService.getCurrentWindowId()}`));
|
||||
this._registry = null;
|
||||
this._installedExtensionsReady = new Barrier();
|
||||
this._isDev = !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment;
|
||||
this._extensionsMessages = {};
|
||||
this._extensionsMessages = new Map<string, IMessage[]>();
|
||||
this._allRequestedActivateEvents = Object.create(null);
|
||||
this._extensionScanner = this._instantiationService.createInstance(CachedExtensionScanner);
|
||||
this._deltaExtensionsQueue = [];
|
||||
|
||||
this._extensionHostProcessManagers = [];
|
||||
this._extensionHostProcessActivationTimes = Object.create(null);
|
||||
this._extensionHostExtensionRuntimeErrors = Object.create(null);
|
||||
this._extensionHostActiveExtensions = new Map<string, ExtensionIdentifier>();
|
||||
this._extensionHostProcessActivationTimes = new Map<string, ActivationTimes>();
|
||||
this._extensionHostExtensionRuntimeErrors = new Map<string, Error[]>();
|
||||
|
||||
this._startDelayed(this._lifecycleService);
|
||||
|
||||
@@ -97,6 +125,255 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
}]);
|
||||
}
|
||||
|
||||
this._extensionEnablementService.onEnablementChanged((extensions) => {
|
||||
let toAdd: IExtension[] = [];
|
||||
let toRemove: string[] = [];
|
||||
for (const extension of extensions) {
|
||||
if (this._extensionEnablementService.isEnabled(extension)) {
|
||||
// an extension has been enabled
|
||||
toAdd.push(extension);
|
||||
} else {
|
||||
// an extension has been disabled
|
||||
toRemove.push(extension.identifier.id);
|
||||
}
|
||||
}
|
||||
this._handleDeltaExtensions(new DeltaExtensionsQueueItem(toAdd, toRemove));
|
||||
});
|
||||
|
||||
this._extensionManagementService.onDidInstallExtension((event) => {
|
||||
if (event.local) {
|
||||
if (this._extensionEnablementService.isEnabled(event.local)) {
|
||||
// an extension has been installed
|
||||
this._handleDeltaExtensions(new DeltaExtensionsQueueItem([event.local], []));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._extensionManagementService.onDidUninstallExtension((event) => {
|
||||
if (!event.error) {
|
||||
// an extension has been uninstalled
|
||||
this._handleDeltaExtensions(new DeltaExtensionsQueueItem([], [event.identifier.id]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _inHandleDeltaExtensions = false;
|
||||
private async _handleDeltaExtensions(item: DeltaExtensionsQueueItem): Promise<void> {
|
||||
this._deltaExtensionsQueue.push(item);
|
||||
if (this._inHandleDeltaExtensions) {
|
||||
// Let the current item finish, the new one will be picked up
|
||||
return;
|
||||
}
|
||||
|
||||
while (this._deltaExtensionsQueue.length > 0) {
|
||||
const item = this._deltaExtensionsQueue.shift();
|
||||
try {
|
||||
this._inHandleDeltaExtensions = true;
|
||||
await this._deltaExtensions(item.toAdd, item.toRemove);
|
||||
} finally {
|
||||
this._inHandleDeltaExtensions = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _deltaExtensions(_toAdd: IExtension[], _toRemove: string[]): Promise<void> {
|
||||
if (this._windowService.getConfiguration().remoteAuthority) {
|
||||
return;
|
||||
}
|
||||
|
||||
let toAdd: IExtensionDescription[] = [];
|
||||
for (let i = 0, len = _toAdd.length; i < len; i++) {
|
||||
const extension = _toAdd[i];
|
||||
|
||||
if (extension.location.scheme !== Schemas.file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingExtensionDescription = this._registry.getExtensionDescription(extension.identifier.id);
|
||||
if (existingExtensionDescription) {
|
||||
// this extension is already running (most likely at a different version)
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionDescription = await this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System, this.createLogger());
|
||||
if (!extensionDescription || !this._usesOnlyDynamicExtensionPoints(extensionDescription)) {
|
||||
// uses non-dynamic extension point
|
||||
continue;
|
||||
}
|
||||
|
||||
toAdd.push(extensionDescription);
|
||||
}
|
||||
|
||||
let toRemove: IExtensionDescription[] = [];
|
||||
for (let i = 0, len = _toRemove.length; i < len; i++) {
|
||||
const extensionId = _toRemove[i];
|
||||
const extensionDescription = this._registry.getExtensionDescription(extensionId);
|
||||
if (!extensionDescription) {
|
||||
// ignore disabling/uninstalling an extension which is not running
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this._canRemoveExtension(extensionDescription)) {
|
||||
// uses non-dynamic extension point or is activated
|
||||
continue;
|
||||
}
|
||||
|
||||
toRemove.push(extensionDescription);
|
||||
}
|
||||
|
||||
if (toAdd.length === 0 && toRemove.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the local registry
|
||||
this._registry.deltaExtensions(toAdd, toRemove.map(e => e.identifier));
|
||||
|
||||
// Update extension points
|
||||
this._rehandleExtensionPoints((<IExtensionDescription[]>[]).concat(toAdd).concat(toRemove));
|
||||
|
||||
// Update the extension host
|
||||
if (this._extensionHostProcessManagers.length > 0) {
|
||||
await this._extensionHostProcessManagers[0].deltaExtensions(toAdd, toRemove.map(e => e.identifier));
|
||||
}
|
||||
|
||||
this._onDidChangeExtensions.fire(undefined);
|
||||
|
||||
for (let i = 0; i < toAdd.length; i++) {
|
||||
this._activateAddedExtensionIfNeeded(toAdd[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private _rehandleExtensionPoints(extensionDescriptions: IExtensionDescription[]): void {
|
||||
const affectedExtensionPoints: { [extPointName: string]: boolean; } = Object.create(null);
|
||||
for (let extensionDescription of extensionDescriptions) {
|
||||
if (extensionDescription.contributes) {
|
||||
for (let extPointName in extensionDescription.contributes) {
|
||||
if (hasOwnProperty.call(extensionDescription.contributes, extPointName)) {
|
||||
affectedExtensionPoints[extPointName] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messageHandler = (msg: IMessage) => this._handleExtensionPointMessage(msg);
|
||||
|
||||
const availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
const extensionPoints = ExtensionsRegistry.getExtensionPoints();
|
||||
for (let i = 0, len = extensionPoints.length; i < len; i++) {
|
||||
if (affectedExtensionPoints[extensionPoints[i].name]) {
|
||||
ExtensionService._handleExtensionPoint(extensionPoints[i], availableExtensions, messageHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _usesOnlyDynamicExtensionPoints(extension: IExtensionDescription): boolean {
|
||||
const extensionPoints = ExtensionsRegistry.getExtensionPointsMap();
|
||||
if (extension.contributes) {
|
||||
for (let extPointName in extension.contributes) {
|
||||
if (hasOwnProperty.call(extension.contributes, extPointName)) {
|
||||
const extPoint = extensionPoints[extPointName];
|
||||
if (extPoint) {
|
||||
if (!extPoint.isDynamic) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// This extension has a 3rd party (unknown) extension point
|
||||
// ===> require a reload for now...
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public canAddExtension(extension: IExtensionDescription): boolean {
|
||||
if (this._windowService.getConfiguration().remoteAuthority) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extension.extensionLocation.scheme !== Schemas.file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extensionDescription = this._registry.getExtensionDescription(extension.identifier);
|
||||
if (extensionDescription) {
|
||||
// ignore adding an extension which is already running and cannot be removed
|
||||
if (!this._canRemoveExtension(extensionDescription)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return this._usesOnlyDynamicExtensionPoints(extension);
|
||||
}
|
||||
|
||||
public canRemoveExtension(extension: IExtensionDescription): boolean {
|
||||
if (this._windowService.getConfiguration().remoteAuthority) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extension.extensionLocation.scheme !== Schemas.file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extensionDescription = this._registry.getExtensionDescription(extension.identifier);
|
||||
if (!extensionDescription) {
|
||||
// ignore removing an extension which is not running
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._canRemoveExtension(extensionDescription);
|
||||
}
|
||||
|
||||
private _canRemoveExtension(extension: IExtensionDescription): boolean {
|
||||
if (this._extensionHostActiveExtensions.has(ExtensionIdentifier.toKey(extension.identifier))) {
|
||||
// Extension is running, cannot remove it safely
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._usesOnlyDynamicExtensionPoints(extension);
|
||||
}
|
||||
|
||||
private async _activateAddedExtensionIfNeeded(extensionDescription: IExtensionDescription): Promise<void> {
|
||||
|
||||
let shouldActivate = false;
|
||||
let shouldActivateReason: string | null = null;
|
||||
if (Array.isArray(extensionDescription.activationEvents)) {
|
||||
for (let activationEvent of extensionDescription.activationEvents) {
|
||||
// TODO@joao: there's no easy way to contribute this
|
||||
if (activationEvent === 'onUri') {
|
||||
activationEvent = `onUri:${ExtensionIdentifier.toKey(extensionDescription.identifier)}`;
|
||||
}
|
||||
|
||||
if (this._allRequestedActivateEvents[activationEvent]) {
|
||||
// This activation event was fired before the extension was added
|
||||
shouldActivate = true;
|
||||
shouldActivateReason = activationEvent;
|
||||
break;
|
||||
}
|
||||
|
||||
if (activationEvent === '*') {
|
||||
shouldActivate = true;
|
||||
shouldActivateReason = activationEvent;
|
||||
break;
|
||||
}
|
||||
|
||||
if (/^workspaceContains/.test(activationEvent)) {
|
||||
// do not trigger a search, just activate in this case...
|
||||
shouldActivate = true;
|
||||
shouldActivateReason = activationEvent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldActivate) {
|
||||
await Promise.all(
|
||||
this._extensionHostProcessManagers.map(extHostManager => extHostManager.activate(extensionDescription.identifier, shouldActivateReason))
|
||||
).then(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
private _startDelayed(lifecycleService: ILifecycleService): void {
|
||||
@@ -137,14 +414,18 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
|
||||
private _stopExtensionHostProcess(): void {
|
||||
const previouslyActivatedExtensionIds = Object.keys(this._extensionHostProcessActivationTimes);
|
||||
let previouslyActivatedExtensionIds: ExtensionIdentifier[] = [];
|
||||
this._extensionHostActiveExtensions.forEach((value) => {
|
||||
previouslyActivatedExtensionIds.push(value);
|
||||
});
|
||||
|
||||
for (let i = 0; i < this._extensionHostProcessManagers.length; i++) {
|
||||
this._extensionHostProcessManagers[i].dispose();
|
||||
for (const manager of this._extensionHostProcessManagers) {
|
||||
manager.dispose();
|
||||
}
|
||||
this._extensionHostProcessManagers = [];
|
||||
this._extensionHostProcessActivationTimes = Object.create(null);
|
||||
this._extensionHostExtensionRuntimeErrors = Object.create(null);
|
||||
this._extensionHostActiveExtensions = new Map<string, ExtensionIdentifier>();
|
||||
this._extensionHostProcessActivationTimes = new Map<string, ActivationTimes>();
|
||||
this._extensionHostExtensionRuntimeErrors = new Map<string, Error[]>();
|
||||
|
||||
if (previouslyActivatedExtensionIds.length > 0) {
|
||||
this._onDidChangeExtensionsStatus.fire(previouslyActivatedExtensionIds);
|
||||
@@ -154,7 +435,18 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
private _startExtensionHostProcess(isInitialStart: boolean, initialActivationEvents: string[]): void {
|
||||
this._stopExtensionHostProcess();
|
||||
|
||||
const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, !isInitialStart, this.getExtensions(), this._extensionHostLogsLocation);
|
||||
let autoStart: boolean;
|
||||
let extensions: Promise<IExtensionDescription[]>;
|
||||
if (isInitialStart) {
|
||||
autoStart = false;
|
||||
extensions = this._extensionScanner.scannedExtensions;
|
||||
} else {
|
||||
// restart case
|
||||
autoStart = true;
|
||||
extensions = this.getExtensions();
|
||||
}
|
||||
|
||||
const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, autoStart, extensions, this._extensionHostLogsLocation);
|
||||
const extHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, extHostProcessWorker, null, initialActivationEvents);
|
||||
extHostProcessManager.onDidCrash(([code, signal]) => this._onExtensionHostCrashed(code, signal));
|
||||
extHostProcessManager.onDidChangeResponsiveState((responsiveState) => { this._onDidChangeResponsiveChange.fire({ target: extHostProcessManager, isResponsive: responsiveState === ResponsiveState.Responsive }); });
|
||||
@@ -205,14 +497,14 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
if (this._installedExtensionsReady.isOpen()) {
|
||||
// Extensions have been scanned and interpreted
|
||||
|
||||
// Record the fact that this activationEvent was requested (in case of a restart)
|
||||
this._allRequestedActivateEvents[activationEvent] = true;
|
||||
|
||||
if (!this._registry.containsActivationEvent(activationEvent)) {
|
||||
// There is no extension that is interested in this activation event
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
|
||||
// Record the fact that this activationEvent was requested (in case of a restart)
|
||||
this._allRequestedActivateEvents[activationEvent] = true;
|
||||
|
||||
return this._activateByEvent(activationEvent);
|
||||
} else {
|
||||
// Extensions have not been scanned yet.
|
||||
@@ -272,13 +564,12 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
let result: { [id: string]: IExtensionsStatus; } = Object.create(null);
|
||||
if (this._registry) {
|
||||
const extensions = this._registry.getAllExtensionDescriptions();
|
||||
for (let i = 0, len = extensions.length; i < len; i++) {
|
||||
const extension = extensions[i];
|
||||
const id = extension.id;
|
||||
result[id] = {
|
||||
messages: this._extensionsMessages[id],
|
||||
activationTimes: this._extensionHostProcessActivationTimes[id],
|
||||
runtimeErrors: this._extensionHostExtensionRuntimeErrors[id],
|
||||
for (const extension of extensions) {
|
||||
const extensionKey = ExtensionIdentifier.toKey(extension.identifier);
|
||||
result[extension.identifier.value] = {
|
||||
messages: this._extensionsMessages.get(extensionKey),
|
||||
activationTimes: this._extensionHostProcessActivationTimes.get(extensionKey),
|
||||
runtimeErrors: this._extensionHostExtensionRuntimeErrors.get(extensionKey),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -316,23 +607,29 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
|
||||
// --- impl
|
||||
|
||||
private async _scanAndHandleExtensions(): Promise<void> {
|
||||
this._extensionScanner.startScanningExtensions(new Logger((severity, source, message) => {
|
||||
private createLogger(): Logger {
|
||||
return new Logger((severity, source, message) => {
|
||||
if (this._isDev && source) {
|
||||
this._logOrShowMessage(severity, `[${source}]: ${message}`);
|
||||
} else {
|
||||
this._logOrShowMessage(severity, message);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private async _scanAndHandleExtensions(): Promise<void> {
|
||||
this._extensionScanner.startScanningExtensions(this.createLogger());
|
||||
|
||||
const extensionHost = this._extensionHostProcessManagers[0];
|
||||
const extensions = await this._extensionScanner.scannedExtensions;
|
||||
const enabledExtensions = await this._getRuntimeExtensions(extensions);
|
||||
extensionHost.start(enabledExtensions.map(extension => extension.id));
|
||||
this._onHasExtensions(enabledExtensions);
|
||||
|
||||
this._handleExtensionPoints(enabledExtensions);
|
||||
extensionHost.start(enabledExtensions.map(extension => extension.identifier));
|
||||
this._releaseBarrier();
|
||||
}
|
||||
|
||||
private _onHasExtensions(allExtensions: IExtensionDescription[]): void {
|
||||
private _handleExtensionPoints(allExtensions: IExtensionDescription[]): void {
|
||||
this._registry = new ExtensionDescriptionRegistry(allExtensions);
|
||||
|
||||
let availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
@@ -343,58 +640,65 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
for (let i = 0, len = extensionPoints.length; i < len; i++) {
|
||||
ExtensionService._handleExtensionPoint(extensionPoints[i], availableExtensions, messageHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private _releaseBarrier(): void {
|
||||
perf.mark('extensionHostReady');
|
||||
this._installedExtensionsReady.open();
|
||||
this._onDidRegisterExtensions.fire(void 0);
|
||||
this._onDidChangeExtensionsStatus.fire(availableExtensions.map(e => e.id));
|
||||
this._onDidRegisterExtensions.fire(undefined);
|
||||
this._onDidChangeExtensionsStatus.fire(this._registry.getAllExtensionDescriptions().map(e => e.identifier));
|
||||
}
|
||||
|
||||
private _getRuntimeExtensions(allExtensions: IExtensionDescription[]): Promise<IExtensionDescription[]> {
|
||||
return this._extensionEnablementService.getDisabledExtensions()
|
||||
.then(disabledExtensions => {
|
||||
|
||||
const result: { [extensionId: string]: IExtensionDescription; } = {};
|
||||
const extensionsToDisable: IExtensionIdentifier[] = [];
|
||||
const runtimeExtensions: IExtensionDescription[] = [];
|
||||
const extensionsToDisable: IExtensionDescription[] = [];
|
||||
const userMigratedSystemExtensions: IExtensionIdentifier[] = [{ id: BetterMergeId }];
|
||||
|
||||
const enableProposedApiFor: string | string[] = this._environmentService.args['enable-proposed-api'] || [];
|
||||
let enableProposedApiFor: string | string[] = this._environmentService.args['enable-proposed-api'] || [];
|
||||
|
||||
const notFound = (id: string) => nls.localize('notFound', "Extension \`{0}\` cannot use PROPOSED API as it cannot be found", id);
|
||||
|
||||
if (enableProposedApiFor.length) {
|
||||
let allProposed = (enableProposedApiFor instanceof Array ? enableProposedApiFor : [enableProposedApiFor]);
|
||||
allProposed.forEach(id => {
|
||||
if (!allExtensions.some(description => description.id === id)) {
|
||||
if (!allExtensions.some(description => ExtensionIdentifier.equals(description.identifier, id))) {
|
||||
console.error(notFound(id));
|
||||
}
|
||||
});
|
||||
// Make enabled proposed API be lowercase for case insensitive comparison
|
||||
if (Array.isArray(enableProposedApiFor)) {
|
||||
enableProposedApiFor = enableProposedApiFor.map(id => id.toLowerCase());
|
||||
} else {
|
||||
enableProposedApiFor = enableProposedApiFor.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
const enableProposedApiForAll = !this._environmentService.isBuilt ||
|
||||
(!!this._environmentService.extensionDevelopmentLocationURI && product.nameLong.indexOf('Insiders') >= 0) ||
|
||||
(!!this._environmentService.extensionDevelopmentLocationURI && product.nameLong !== 'Visual Studio Code') ||
|
||||
(enableProposedApiFor.length === 0 && 'enable-proposed-api' in this._environmentService.args);
|
||||
|
||||
for (const extension of allExtensions) {
|
||||
const isExtensionUnderDevelopment = this._environmentService.isExtensionDevelopment && isEqualOrParent(extension.extensionLocation, this._environmentService.extensionDevelopmentLocationURI);
|
||||
// Do not disable extensions under development
|
||||
if (!isExtensionUnderDevelopment) {
|
||||
if (disabledExtensions.some(disabled => areSameExtensions(disabled, extension))) {
|
||||
if (disabledExtensions.some(disabled => areSameExtensions(disabled, { id: extension.identifier.value }))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!extension.isBuiltin) {
|
||||
// Check if the extension is changed to system extension
|
||||
const userMigratedSystemExtension = userMigratedSystemExtensions.filter(userMigratedSystemExtension => areSameExtensions(userMigratedSystemExtension, { id: extension.id }))[0];
|
||||
const userMigratedSystemExtension = userMigratedSystemExtensions.filter(userMigratedSystemExtension => areSameExtensions(userMigratedSystemExtension, { id: extension.identifier.value }))[0];
|
||||
if (userMigratedSystemExtension) {
|
||||
extensionsToDisable.push(userMigratedSystemExtension);
|
||||
extensionsToDisable.push(extension);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result[extension.id] = this._updateEnableProposedApi(extension, enableProposedApiForAll, enableProposedApiFor);
|
||||
runtimeExtensions.push(this._updateEnableProposedApi(extension, enableProposedApiForAll, enableProposedApiFor));
|
||||
}
|
||||
const runtimeExtensions = Object.keys(result).map(name => result[name]);
|
||||
|
||||
this._telemetryService.publicLog('extensionsScanned', {
|
||||
totalCount: runtimeExtensions.length,
|
||||
@@ -402,14 +706,8 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
});
|
||||
|
||||
if (extensionsToDisable.length) {
|
||||
return this._extensionManagementService.getInstalled(LocalExtensionType.User)
|
||||
.then(installed => {
|
||||
const toDisable = installed.filter(i => extensionsToDisable.some(e => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i) }, e)));
|
||||
return Promise.all(toDisable.map(e => this._extensionEnablementService.setEnablement(e, EnablementState.Disabled)));
|
||||
})
|
||||
.then(() => {
|
||||
return runtimeExtensions;
|
||||
});
|
||||
return this._extensionEnablementService.setEnablement(extensionsToDisable.map(e => toExtension(e)), EnablementState.Disabled)
|
||||
.then(() => runtimeExtensions);
|
||||
} else {
|
||||
return runtimeExtensions;
|
||||
}
|
||||
@@ -417,9 +715,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
|
||||
private _updateEnableProposedApi(extension: IExtensionDescription, enableProposedApiForAll: boolean, enableProposedApiFor: string | string[]): IExtensionDescription {
|
||||
if (isNonEmptyArray(product.extensionAllowedProposedApi)
|
||||
&& product.extensionAllowedProposedApi.indexOf(extension.id) >= 0
|
||||
) {
|
||||
if (allowProposedApiFromProduct(extension.identifier)) {
|
||||
// fast lane -> proposed api is available to all extensions
|
||||
// that are listed in product.json-files
|
||||
extension.enableProposedApi = true;
|
||||
@@ -427,29 +723,30 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
} else if (extension.enableProposedApi && !extension.isBuiltin) {
|
||||
if (
|
||||
!enableProposedApiForAll &&
|
||||
enableProposedApiFor.indexOf(extension.id) < 0
|
||||
enableProposedApiFor.indexOf(extension.identifier.value.toLowerCase()) < 0
|
||||
) {
|
||||
extension.enableProposedApi = false;
|
||||
console.error(`Extension '${extension.id} cannot use PROPOSED API (must started out of dev or enabled via --enable-proposed-api)`);
|
||||
console.error(`Extension '${extension.identifier.value} cannot use PROPOSED API (must started out of dev or enabled via --enable-proposed-api)`);
|
||||
|
||||
} else {
|
||||
// proposed api is available when developing or when an extension was explicitly
|
||||
// spelled out via a command line argument
|
||||
console.warn(`Extension '${extension.id}' uses PROPOSED API which is subject to change and removal without notice.`);
|
||||
console.warn(`Extension '${extension.identifier.value}' uses PROPOSED API which is subject to change and removal without notice.`);
|
||||
}
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
|
||||
private _handleExtensionPointMessage(msg: IMessage) {
|
||||
const extensionKey = ExtensionIdentifier.toKey(msg.extensionId);
|
||||
|
||||
if (!this._extensionsMessages[msg.extensionId]) {
|
||||
this._extensionsMessages[msg.extensionId] = [];
|
||||
if (!this._extensionsMessages.has(extensionKey)) {
|
||||
this._extensionsMessages.set(extensionKey, []);
|
||||
}
|
||||
this._extensionsMessages[msg.extensionId].push(msg);
|
||||
this._extensionsMessages.get(extensionKey).push(msg);
|
||||
|
||||
const extension = this._registry.getExtensionDescription(msg.extensionId);
|
||||
const strMsg = `[${msg.extensionId}]: ${msg.message}`;
|
||||
const strMsg = `[${msg.extensionId.value}]: ${msg.message}`;
|
||||
if (extension && extension.isUnderDevelopment) {
|
||||
// This message is about the extension currently being developed
|
||||
this._showMessageToUser(msg.type, strMsg);
|
||||
@@ -468,7 +765,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
*/
|
||||
this._telemetryService.publicLog('extensionsMessage', {
|
||||
type, extensionId, extensionPointId, message
|
||||
type, extensionId: extensionId.value, extensionPointId, message
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -518,24 +815,30 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
}
|
||||
|
||||
public _onExtensionActivated(extensionId: string, startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationEvent: string): void {
|
||||
this._extensionHostProcessActivationTimes[extensionId] = new ActivationTimes(startup, codeLoadingTime, activateCallTime, activateResolvedTime, activationEvent);
|
||||
public _onWillActivateExtension(extensionId: ExtensionIdentifier): void {
|
||||
this._extensionHostActiveExtensions.set(ExtensionIdentifier.toKey(extensionId), extensionId);
|
||||
}
|
||||
|
||||
public _onDidActivateExtension(extensionId: ExtensionIdentifier, startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationEvent: string): void {
|
||||
this._extensionHostProcessActivationTimes.set(ExtensionIdentifier.toKey(extensionId), new ActivationTimes(startup, codeLoadingTime, activateCallTime, activateResolvedTime, activationEvent));
|
||||
this._onDidChangeExtensionsStatus.fire([extensionId]);
|
||||
}
|
||||
|
||||
public _onExtensionRuntimeError(extensionId: string, err: Error): void {
|
||||
if (!this._extensionHostExtensionRuntimeErrors[extensionId]) {
|
||||
this._extensionHostExtensionRuntimeErrors[extensionId] = [];
|
||||
public _onExtensionRuntimeError(extensionId: ExtensionIdentifier, err: Error): void {
|
||||
const extensionKey = ExtensionIdentifier.toKey(extensionId);
|
||||
if (!this._extensionHostExtensionRuntimeErrors.has(extensionKey)) {
|
||||
this._extensionHostExtensionRuntimeErrors.set(extensionKey, []);
|
||||
}
|
||||
this._extensionHostExtensionRuntimeErrors[extensionId].push(err);
|
||||
this._extensionHostExtensionRuntimeErrors.get(extensionKey).push(err);
|
||||
this._onDidChangeExtensionsStatus.fire([extensionId]);
|
||||
}
|
||||
|
||||
public _addMessage(extensionId: string, severity: Severity, message: string): void {
|
||||
if (!this._extensionsMessages[extensionId]) {
|
||||
this._extensionsMessages[extensionId] = [];
|
||||
public _addMessage(extensionId: ExtensionIdentifier, severity: Severity, message: string): void {
|
||||
const extensionKey = ExtensionIdentifier.toKey(extensionId);
|
||||
if (!this._extensionsMessages.has(extensionKey)) {
|
||||
this._extensionsMessages.set(extensionKey, []);
|
||||
}
|
||||
this._extensionsMessages[extensionId].push({
|
||||
this._extensionsMessages.get(extensionKey).push({
|
||||
type: severity,
|
||||
message: message,
|
||||
extensionId: null,
|
||||
|
||||
@@ -7,7 +7,6 @@ import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { EnablementState, IExtensionEnablementService, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
@@ -17,6 +16,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
|
||||
import { IURLHandler, IURLService } from 'vs/platform/url/common/url';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||
const THIRTY_SECONDS = 30 * 1000;
|
||||
@@ -30,8 +30,8 @@ export const IExtensionUrlHandler = createDecorator<IExtensionUrlHandler>('inact
|
||||
|
||||
export interface IExtensionUrlHandler {
|
||||
readonly _serviceBrand: any;
|
||||
registerExtensionHandler(extensionId: string, handler: IURLHandler): void;
|
||||
unregisterExtensionHandler(extensionId: string): void;
|
||||
registerExtensionHandler(extensionId: ExtensionIdentifier, handler: IURLHandler): void;
|
||||
unregisterExtensionHandler(extensionId: ExtensionIdentifier): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,14 +53,14 @@ export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
|
||||
|
||||
constructor(
|
||||
@IURLService urlService: IURLService,
|
||||
@IExtensionService private extensionService: IExtensionService,
|
||||
@IDialogService private dialogService: IDialogService,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@IExtensionManagementService private extensionManagementService: IExtensionManagementService,
|
||||
@IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService,
|
||||
@IWindowService private windowService: IWindowService,
|
||||
@IExtensionGalleryService private galleryService: IExtensionGalleryService,
|
||||
@IStorageService private storageService: IStorageService
|
||||
@IExtensionService private readonly extensionService: IExtensionService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService,
|
||||
@IWindowService private readonly windowService: IWindowService,
|
||||
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
|
||||
@IStorageService private readonly storageService: IStorageService
|
||||
) {
|
||||
const interval = setInterval(() => this.garbageCollect(), THIRTY_SECONDS);
|
||||
const urlToHandleValue = this.storageService.get(URL_TO_HANDLE, StorageScope.WORKSPACE);
|
||||
@@ -75,87 +75,80 @@ export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
|
||||
]);
|
||||
}
|
||||
|
||||
handleURL(uri: URI, confirmed?: boolean): TPromise<boolean> {
|
||||
async handleURL(uri: URI, confirmed?: boolean): Promise<boolean> {
|
||||
if (!isExtensionId(uri.authority)) {
|
||||
return TPromise.as(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const extensionId = uri.authority;
|
||||
const wasHandlerAvailable = this.extensionHandlers.has(extensionId);
|
||||
const wasHandlerAvailable = this.extensionHandlers.has(ExtensionIdentifier.toKey(extensionId));
|
||||
const extension = await this.extensionService.getExtension(extensionId);
|
||||
|
||||
return this.extensionService.getExtension(extensionId).then(extension => {
|
||||
if (!extension) {
|
||||
await this.handleUnhandledURL(uri, { id: extensionId });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!extension) {
|
||||
return this.handleUnhandledURL(uri, { id: extensionId }).then(() => false);
|
||||
}
|
||||
|
||||
const handleURL = () => {
|
||||
const handler = this.extensionHandlers.get(extensionId);
|
||||
if (handler) {
|
||||
if (!wasHandlerAvailable) {
|
||||
// forward it directly
|
||||
return handler.handleURL(uri);
|
||||
}
|
||||
|
||||
// let the ExtensionUrlHandler instance handle this
|
||||
return TPromise.as(false);
|
||||
}
|
||||
|
||||
// collect URI for eventual extension activation
|
||||
const timestamp = new Date().getTime();
|
||||
let uris = this.uriBuffer.get(extensionId);
|
||||
|
||||
if (!uris) {
|
||||
uris = [];
|
||||
this.uriBuffer.set(extensionId, uris);
|
||||
}
|
||||
|
||||
uris.push({ timestamp, uri });
|
||||
|
||||
// activate the extension
|
||||
return this.extensionService.activateByEvent(`onUri:${extensionId}`)
|
||||
.then(() => true);
|
||||
};
|
||||
|
||||
if (confirmed) {
|
||||
return handleURL();
|
||||
}
|
||||
|
||||
return this.dialogService.confirm({
|
||||
if (!confirmed) {
|
||||
const result = await this.dialogService.confirm({
|
||||
message: localize('confirmUrl', "Allow an extension to open this URL?", extensionId),
|
||||
detail: `${extension.displayName || extension.name} (${extensionId}) wants to open a URL:\n\n${uri.toString()}`,
|
||||
primaryButton: localize('open', "&&Open"),
|
||||
type: 'question'
|
||||
}).then(result => {
|
||||
|
||||
if (!result.confirmed) {
|
||||
return TPromise.as(true);
|
||||
}
|
||||
|
||||
return handleURL();
|
||||
});
|
||||
});
|
||||
|
||||
if (!result.confirmed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const handler = this.extensionHandlers.get(ExtensionIdentifier.toKey(extensionId));
|
||||
|
||||
if (handler) {
|
||||
if (!wasHandlerAvailable) {
|
||||
// forward it directly
|
||||
return await handler.handleURL(uri);
|
||||
}
|
||||
|
||||
// let the ExtensionUrlHandler instance handle this
|
||||
return false;
|
||||
}
|
||||
|
||||
// collect URI for eventual extension activation
|
||||
const timestamp = new Date().getTime();
|
||||
let uris = this.uriBuffer.get(ExtensionIdentifier.toKey(extensionId));
|
||||
|
||||
if (!uris) {
|
||||
uris = [];
|
||||
this.uriBuffer.set(ExtensionIdentifier.toKey(extensionId), uris);
|
||||
}
|
||||
|
||||
uris.push({ timestamp, uri });
|
||||
|
||||
// activate the extension
|
||||
await this.extensionService.activateByEvent(`onUri:${ExtensionIdentifier.toKey(extensionId)}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
registerExtensionHandler(extensionId: string, handler: IURLHandler): void {
|
||||
this.extensionHandlers.set(extensionId, handler);
|
||||
registerExtensionHandler(extensionId: ExtensionIdentifier, handler: IURLHandler): void {
|
||||
this.extensionHandlers.set(ExtensionIdentifier.toKey(extensionId), handler);
|
||||
|
||||
const uris = this.uriBuffer.get(extensionId) || [];
|
||||
const uris = this.uriBuffer.get(ExtensionIdentifier.toKey(extensionId)) || [];
|
||||
|
||||
for (const { uri } of uris) {
|
||||
handler.handleURL(uri);
|
||||
}
|
||||
|
||||
this.uriBuffer.delete(extensionId);
|
||||
this.uriBuffer.delete(ExtensionIdentifier.toKey(extensionId));
|
||||
}
|
||||
|
||||
unregisterExtensionHandler(extensionId: string): void {
|
||||
this.extensionHandlers.delete(extensionId);
|
||||
unregisterExtensionHandler(extensionId: ExtensionIdentifier): void {
|
||||
this.extensionHandlers.delete(ExtensionIdentifier.toKey(extensionId));
|
||||
}
|
||||
|
||||
private async handleUnhandledURL(uri: URI, extensionIdentifier: IExtensionIdentifier): Promise<void> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const extension = installedExtensions.filter(e => areSameExtensions(e.galleryIdentifier, extensionIdentifier))[0];
|
||||
const extension = installedExtensions.filter(e => areSameExtensions(e.identifier, extensionIdentifier))[0];
|
||||
|
||||
// Extension is installed
|
||||
if (extension) {
|
||||
@@ -163,88 +156,91 @@ export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
|
||||
|
||||
// Extension is not running. Reload the window to handle.
|
||||
if (enabled) {
|
||||
this.dialogService.confirm({
|
||||
const result = await this.dialogService.confirm({
|
||||
message: localize('reloadAndHandle', "Extension '{0}' is not loaded. Would you like to reload the window to load the extension and open the URL?", extension.manifest.displayName || extension.manifest.name),
|
||||
detail: `${extension.manifest.displayName || extension.manifest.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`,
|
||||
primaryButton: localize('reloadAndOpen', "&&Reload Window and Open"),
|
||||
type: 'question'
|
||||
}).then(result => {
|
||||
if (result.confirmed) {
|
||||
return this.reloadAndHandle(uri);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!result.confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.reloadAndHandle(uri);
|
||||
}
|
||||
|
||||
// Extension is disabled. Enable the extension and reload the window to handle.
|
||||
else {
|
||||
this.dialogService.confirm({
|
||||
const result = await this.dialogService.confirm({
|
||||
message: localize('enableAndHandle', "Extension '{0}' is disabled. Would you like to enable the extension and reload the window to open the URL?", extension.manifest.displayName || extension.manifest.name),
|
||||
detail: `${extension.manifest.displayName || extension.manifest.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`,
|
||||
primaryButton: localize('enableAndReload', "&&Enable and Open"),
|
||||
type: 'question'
|
||||
}).then((result): TPromise<void> | null => {
|
||||
if (result.confirmed) {
|
||||
return this.extensionEnablementService.setEnablement(extension, EnablementState.Enabled)
|
||||
.then(() => this.reloadAndHandle(uri));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!result.confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.extensionEnablementService.setEnablement([extension], EnablementState.Enabled);
|
||||
await this.reloadAndHandle(uri);
|
||||
}
|
||||
}
|
||||
|
||||
// Extension is not installed
|
||||
else {
|
||||
const galleryExtension = await this.galleryService.getExtension(extensionIdentifier);
|
||||
if (galleryExtension) {
|
||||
// Install the Extension and reload the window to handle.
|
||||
this.dialogService.confirm({
|
||||
message: localize('installAndHandle', "Extension '{0}' is not installed. Would you like to install the extension and reload the window to open this URL?", galleryExtension.displayName || galleryExtension.name),
|
||||
detail: `${galleryExtension.displayName || galleryExtension.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`,
|
||||
primaryButton: localize('install', "&&Install"),
|
||||
type: 'question'
|
||||
}).then(async result => {
|
||||
if (result.confirmed) {
|
||||
let notificationHandle: INotificationHandle | null = this.notificationService.notify({ severity: Severity.Info, message: localize('Installing', "Installing Extension '{0}'...", galleryExtension.displayName || galleryExtension.name) });
|
||||
notificationHandle.progress.infinite();
|
||||
notificationHandle.onDidClose(() => notificationHandle = null);
|
||||
try {
|
||||
await this.extensionManagementService.installFromGallery(galleryExtension);
|
||||
const reloadMessage = localize('reload', "Would you like to reload the window and open the URL '{0}'?", uri.toString());
|
||||
const reloadActionLabel = localize('Reload', "Reload Window and Open");
|
||||
if (notificationHandle) {
|
||||
notificationHandle.progress.done();
|
||||
notificationHandle.updateMessage(reloadMessage);
|
||||
notificationHandle.updateActions({
|
||||
primary: [new Action('reloadWindow', reloadActionLabel, undefined, true, () => this.reloadAndHandle(uri))]
|
||||
});
|
||||
} else {
|
||||
this.notificationService.prompt(Severity.Info, reloadMessage,
|
||||
[{
|
||||
label: reloadActionLabel,
|
||||
run: () => this.reloadAndHandle(uri)
|
||||
}],
|
||||
{ sticky: true }
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (notificationHandle) {
|
||||
notificationHandle.progress.done();
|
||||
notificationHandle.updateSeverity(Severity.Error);
|
||||
notificationHandle.updateMessage(e);
|
||||
} else {
|
||||
this.notificationService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const galleryExtension = await this.galleryService.getCompatibleExtension(extensionIdentifier);
|
||||
|
||||
if (!galleryExtension) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Install the Extension and reload the window to handle.
|
||||
const result = await this.dialogService.confirm({
|
||||
message: localize('installAndHandle', "Extension '{0}' is not installed. Would you like to install the extension and reload the window to open this URL?", galleryExtension.displayName || galleryExtension.name),
|
||||
detail: `${galleryExtension.displayName || galleryExtension.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`,
|
||||
primaryButton: localize('install', "&&Install"),
|
||||
type: 'question'
|
||||
});
|
||||
|
||||
if (!result.confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let notificationHandle: INotificationHandle | null = this.notificationService.notify({ severity: Severity.Info, message: localize('Installing', "Installing Extension '{0}'...", galleryExtension.displayName || galleryExtension.name) });
|
||||
notificationHandle.progress.infinite();
|
||||
notificationHandle.onDidClose(() => notificationHandle = null);
|
||||
|
||||
try {
|
||||
await this.extensionManagementService.installFromGallery(galleryExtension);
|
||||
const reloadMessage = localize('reload', "Would you like to reload the window and open the URL '{0}'?", uri.toString());
|
||||
const reloadActionLabel = localize('Reload', "Reload Window and Open");
|
||||
|
||||
if (notificationHandle) {
|
||||
notificationHandle.progress.done();
|
||||
notificationHandle.updateMessage(reloadMessage);
|
||||
notificationHandle.updateActions({
|
||||
primary: [new Action('reloadWindow', reloadActionLabel, undefined, true, () => this.reloadAndHandle(uri))]
|
||||
});
|
||||
} else {
|
||||
this.notificationService.prompt(Severity.Info, reloadMessage, [{ label: reloadActionLabel, run: () => this.reloadAndHandle(uri) }], { sticky: true });
|
||||
}
|
||||
} catch (e) {
|
||||
if (notificationHandle) {
|
||||
notificationHandle.progress.done();
|
||||
notificationHandle.updateSeverity(Severity.Error);
|
||||
notificationHandle.updateMessage(e);
|
||||
} else {
|
||||
this.notificationService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private reloadAndHandle(url: URI): TPromise<void> {
|
||||
private async reloadAndHandle(url: URI): Promise<void> {
|
||||
this.storageService.store(URL_TO_HANDLE, JSON.stringify(url.toJSON()), StorageScope.WORKSPACE);
|
||||
return this.windowService.reloadWindow();
|
||||
await this.windowService.reloadWindow();
|
||||
}
|
||||
|
||||
// forget about all uris buffered more than 5 minutes ago
|
||||
|
||||
@@ -30,7 +30,7 @@ export class RuntimeExtensionsInput extends EditorInput {
|
||||
return true;
|
||||
}
|
||||
|
||||
resolve(): Thenable<any> {
|
||||
resolve(): Promise<any> {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
|
||||
export class ExtensionDescriptionRegistry {
|
||||
private readonly _onDidChange = new Emitter<void>();
|
||||
public readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
private _extensionDescriptions: IExtensionDescription[];
|
||||
private _extensionsMap: { [extensionId: string]: IExtensionDescription; };
|
||||
private _extensionsMap: Map<string, IExtensionDescription>;
|
||||
private _extensionsArr: IExtensionDescription[];
|
||||
private _activationMap: { [activationEvent: string]: IExtensionDescription[]; };
|
||||
private _activationMap: Map<string, IExtensionDescription[]>;
|
||||
|
||||
constructor(extensionDescriptions: IExtensionDescription[]) {
|
||||
this._extensionDescriptions = extensionDescriptions;
|
||||
@@ -19,64 +22,68 @@ export class ExtensionDescriptionRegistry {
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._extensionsMap = {};
|
||||
this._extensionsMap = new Map<string, IExtensionDescription>();
|
||||
this._extensionsArr = [];
|
||||
this._activationMap = {};
|
||||
this._activationMap = new Map<string, IExtensionDescription[]>();
|
||||
|
||||
for (let i = 0, len = this._extensionDescriptions.length; i < len; i++) {
|
||||
let extensionDescription = this._extensionDescriptions[i];
|
||||
|
||||
if (hasOwnProperty.call(this._extensionsMap, extensionDescription.id)) {
|
||||
for (const extensionDescription of this._extensionDescriptions) {
|
||||
if (this._extensionsMap.has(ExtensionIdentifier.toKey(extensionDescription.identifier))) {
|
||||
// No overwriting allowed!
|
||||
console.error('Extension `' + extensionDescription.id + '` is already registered');
|
||||
console.error('Extension `' + extensionDescription.identifier.value + '` is already registered');
|
||||
continue;
|
||||
}
|
||||
|
||||
this._extensionsMap[extensionDescription.id] = extensionDescription;
|
||||
this._extensionsMap.set(ExtensionIdentifier.toKey(extensionDescription.identifier), extensionDescription);
|
||||
this._extensionsArr.push(extensionDescription);
|
||||
|
||||
if (Array.isArray(extensionDescription.activationEvents)) {
|
||||
for (let j = 0, lenJ = extensionDescription.activationEvents.length; j < lenJ; j++) {
|
||||
let activationEvent = extensionDescription.activationEvents[j];
|
||||
|
||||
for (let activationEvent of extensionDescription.activationEvents) {
|
||||
// TODO@joao: there's no easy way to contribute this
|
||||
if (activationEvent === 'onUri') {
|
||||
activationEvent = `onUri:${extensionDescription.id}`;
|
||||
activationEvent = `onUri:${ExtensionIdentifier.toKey(extensionDescription.identifier)}`;
|
||||
}
|
||||
|
||||
this._activationMap[activationEvent] = this._activationMap[activationEvent] || [];
|
||||
this._activationMap[activationEvent].push(extensionDescription);
|
||||
if (!this._activationMap.has(activationEvent)) {
|
||||
this._activationMap.set(activationEvent, []);
|
||||
}
|
||||
this._activationMap.get(activationEvent)!.push(extensionDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public keepOnly(extensionIds: string[]): void {
|
||||
let toKeep = new Set<string>();
|
||||
extensionIds.forEach(extensionId => toKeep.add(extensionId));
|
||||
this._extensionDescriptions = this._extensionDescriptions.filter(extension => toKeep.has(extension.id));
|
||||
public keepOnly(extensionIds: ExtensionIdentifier[]): void {
|
||||
const toKeep = new Set<string>();
|
||||
extensionIds.forEach(extensionId => toKeep.add(ExtensionIdentifier.toKey(extensionId)));
|
||||
this._extensionDescriptions = this._extensionDescriptions.filter(extension => toKeep.has(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
this._initialize();
|
||||
this._onDidChange.fire(undefined);
|
||||
}
|
||||
|
||||
public deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]) {
|
||||
this._extensionDescriptions = this._extensionDescriptions.concat(toAdd);
|
||||
const toRemoveSet = new Set<string>();
|
||||
toRemove.forEach(extensionId => toRemoveSet.add(ExtensionIdentifier.toKey(extensionId)));
|
||||
this._extensionDescriptions = this._extensionDescriptions.filter(extension => !toRemoveSet.has(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
this._initialize();
|
||||
this._onDidChange.fire(undefined);
|
||||
}
|
||||
|
||||
public containsActivationEvent(activationEvent: string): boolean {
|
||||
return hasOwnProperty.call(this._activationMap, activationEvent);
|
||||
return this._activationMap.has(activationEvent);
|
||||
}
|
||||
|
||||
public getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[] {
|
||||
if (!hasOwnProperty.call(this._activationMap, activationEvent)) {
|
||||
return [];
|
||||
}
|
||||
return this._activationMap[activationEvent].slice(0);
|
||||
const extensions = this._activationMap.get(activationEvent);
|
||||
return extensions ? extensions.slice(0) : [];
|
||||
}
|
||||
|
||||
public getAllExtensionDescriptions(): IExtensionDescription[] {
|
||||
return this._extensionsArr.slice(0);
|
||||
}
|
||||
|
||||
public getExtensionDescription(extensionId: string): IExtensionDescription | null {
|
||||
if (!hasOwnProperty.call(this._extensionsMap, extensionId)) {
|
||||
return null;
|
||||
}
|
||||
return this._extensionsMap[extensionId];
|
||||
public getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | null {
|
||||
const extension = this._extensionsMap.get(ExtensionIdentifier.toKey(extensionId));
|
||||
return extension ? extension : null;
|
||||
}
|
||||
}
|
||||
|
||||
162
src/vs/workbench/services/extensions/node/extensionHostMain.ts
Normal file
162
src/vs/workbench/services/extensions/node/extensionHostMain.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Counter } from 'vs/base/common/numbers';
|
||||
import { URI, setUriThrowOnMissingScheme } from 'vs/base/common/uri';
|
||||
import { IURITransformer } from 'vs/base/common/uriIpc';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { IEnvironment, IInitData, MainContext, MainThreadConsoleShape } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration';
|
||||
import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';
|
||||
import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService';
|
||||
import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol';
|
||||
|
||||
// we don't (yet) throw when extensions parse
|
||||
// uris that have no scheme
|
||||
setUriThrowOnMissingScheme(false);
|
||||
|
||||
const nativeExit = process.exit.bind(process);
|
||||
function patchProcess(allowExit: boolean) {
|
||||
process.exit = function (code?: number) {
|
||||
if (allowExit) {
|
||||
exit(code);
|
||||
} else {
|
||||
const err = new Error('An extension called process.exit() and this was prevented.');
|
||||
console.warn(err.stack);
|
||||
}
|
||||
} as (code?: number) => never;
|
||||
|
||||
process.crash = function () {
|
||||
const err = new Error('An extension called process.crash() and this was prevented.');
|
||||
console.warn(err.stack);
|
||||
};
|
||||
}
|
||||
|
||||
export function exit(code?: number) {
|
||||
nativeExit(code);
|
||||
}
|
||||
|
||||
export class ExtensionHostMain {
|
||||
|
||||
|
||||
private _isTerminating: boolean;
|
||||
private readonly _environment: IEnvironment;
|
||||
private readonly _extensionService: ExtHostExtensionService;
|
||||
private readonly _extHostConfiguration: ExtHostConfiguration;
|
||||
private readonly _extHostLogService: ExtHostLogService;
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
private _searchRequestIdProvider: Counter;
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol, initData: IInitData) {
|
||||
this._isTerminating = false;
|
||||
const uriTransformer: IURITransformer = null;
|
||||
const rpcProtocol = new RPCProtocol(protocol, null, uriTransformer);
|
||||
|
||||
// ensure URIs are transformed and revived
|
||||
initData = this.transform(initData, rpcProtocol);
|
||||
this._environment = initData.environment;
|
||||
|
||||
const allowExit = !!this._environment.extensionTestsPath; // to support other test frameworks like Jasmin that use process.exit (https://github.com/Microsoft/vscode/issues/37708)
|
||||
patchProcess(allowExit);
|
||||
|
||||
this._patchPatchedConsole(rpcProtocol.getProxy(MainContext.MainThreadConsole));
|
||||
|
||||
// services
|
||||
this._extHostLogService = new ExtHostLogService(initData.logLevel, initData.logsLocation.fsPath);
|
||||
this.disposables.push(this._extHostLogService);
|
||||
|
||||
this._searchRequestIdProvider = new Counter();
|
||||
const extHostWorkspace = new ExtHostWorkspace(rpcProtocol, initData.workspace, this._extHostLogService, this._searchRequestIdProvider);
|
||||
|
||||
this._extHostLogService.info('extension host started');
|
||||
this._extHostLogService.trace('initData', initData);
|
||||
|
||||
this._extHostConfiguration = new ExtHostConfiguration(rpcProtocol.getProxy(MainContext.MainThreadConfiguration), extHostWorkspace);
|
||||
this._extensionService = new ExtHostExtensionService(nativeExit, initData, rpcProtocol, extHostWorkspace, this._extHostConfiguration, this._extHostLogService);
|
||||
|
||||
// error forwarding and stack trace scanning
|
||||
Error.stackTraceLimit = 100; // increase number of stack frames (from 10, https://github.com/v8/v8/wiki/Stack-Trace-API)
|
||||
const extensionErrors = new WeakMap<Error, IExtensionDescription>();
|
||||
this._extensionService.getExtensionPathIndex().then(map => {
|
||||
(<any>Error).prepareStackTrace = (error: Error, stackTrace: errors.V8CallSite[]) => {
|
||||
let stackTraceMessage = '';
|
||||
let extension: IExtensionDescription;
|
||||
let fileName: string;
|
||||
for (const call of stackTrace) {
|
||||
stackTraceMessage += `\n\tat ${call.toString()}`;
|
||||
fileName = call.getFileName();
|
||||
if (!extension && fileName) {
|
||||
extension = map.findSubstr(fileName);
|
||||
}
|
||||
|
||||
}
|
||||
extensionErrors.set(error, extension);
|
||||
return `${error.name || 'Error'}: ${error.message || ''}${stackTraceMessage}`;
|
||||
};
|
||||
});
|
||||
|
||||
const mainThreadExtensions = rpcProtocol.getProxy(MainContext.MainThreadExtensionService);
|
||||
const mainThreadErrors = rpcProtocol.getProxy(MainContext.MainThreadErrors);
|
||||
errors.setUnexpectedErrorHandler(err => {
|
||||
const data = errors.transformErrorForSerialization(err);
|
||||
const extension = extensionErrors.get(err);
|
||||
if (extension) {
|
||||
mainThreadExtensions.$onExtensionRuntimeError(extension.identifier, data);
|
||||
} else {
|
||||
mainThreadErrors.$onUnexpectedError(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _patchPatchedConsole(mainThreadConsole: MainThreadConsoleShape): void {
|
||||
// The console is already patched to use `process.send()`
|
||||
const nativeProcessSend = process.send;
|
||||
process.send = (...args: any[]) => {
|
||||
if (args.length === 0 || !args[0] || args[0].type !== '__$console') {
|
||||
return nativeProcessSend.apply(process, args);
|
||||
}
|
||||
|
||||
mainThreadConsole.$logExtensionHostMessage(args[0]);
|
||||
};
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
if (this._isTerminating) {
|
||||
// we are already shutting down...
|
||||
return;
|
||||
}
|
||||
this._isTerminating = true;
|
||||
|
||||
this.disposables = dispose(this.disposables);
|
||||
|
||||
errors.setUnexpectedErrorHandler((err) => {
|
||||
// TODO: write to log once we have one
|
||||
});
|
||||
|
||||
const extensionsDeactivated = this._extensionService.deactivateAll();
|
||||
|
||||
// Give extensions 1 second to wrap up any async dispose, then exit in at most 4 seconds
|
||||
setTimeout(() => {
|
||||
Promise.race([timeout(4000), extensionsDeactivated]).then(() => exit(), () => exit());
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private transform(initData: IInitData, rpcProtocol: RPCProtocol): IInitData {
|
||||
initData.extensions.forEach((ext) => (<any>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation)));
|
||||
initData.environment.appRoot = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appRoot));
|
||||
initData.environment.appSettingsHome = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appSettingsHome));
|
||||
initData.environment.extensionDevelopmentLocationURI = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.extensionDevelopmentLocationURI));
|
||||
initData.environment.globalStorageHome = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.globalStorageHome));
|
||||
initData.logsLocation = URI.revive(rpcProtocol.transformIncomingURIs(initData.logsLocation));
|
||||
initData.workspace = rpcProtocol.transformIncomingURIs(initData.workspace);
|
||||
return initData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nativeWatchdog from 'native-watchdog';
|
||||
import { createConnection } from 'net';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { Protocol } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import product from 'vs/platform/node/product';
|
||||
import { IInitData } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/common/extensionHostProtocol';
|
||||
import { exit, ExtensionHostMain } from 'vs/workbench/services/extensions/node/extensionHostMain';
|
||||
|
||||
// With Electron 2.x and node.js 8.x the "natives" module
|
||||
// can cause a native crash (see https://github.com/nodejs/node/issues/19891 and
|
||||
// https://github.com/electron/electron/issues/10905). To prevent this from
|
||||
// happening we essentially blocklist this module from getting loaded in any
|
||||
// extension by patching the node require() function.
|
||||
(function () {
|
||||
const Module = require.__$__nodeRequire('module') as any;
|
||||
const originalLoad = Module._load;
|
||||
|
||||
Module._load = function (request) {
|
||||
if (request === 'natives') {
|
||||
throw new Error('Either the extension or a NPM dependency is using the "natives" node module which is unsupported as it can cause a crash of the extension host. Click [here](https://go.microsoft.com/fwlink/?linkid=871887) to find out more');
|
||||
}
|
||||
|
||||
return originalLoad.apply(this, arguments);
|
||||
};
|
||||
})();
|
||||
|
||||
interface IRendererConnection {
|
||||
protocol: IMessagePassingProtocol;
|
||||
initData: IInitData;
|
||||
}
|
||||
|
||||
// This calls exit directly in case the initialization is not finished and we need to exit
|
||||
// Otherwise, if initialization completed we go to extensionHostMain.terminate()
|
||||
let onTerminate = function () {
|
||||
exit();
|
||||
};
|
||||
|
||||
function createExtHostProtocol(): Promise<IMessagePassingProtocol> {
|
||||
|
||||
const pipeName = process.env.VSCODE_IPC_HOOK_EXTHOST;
|
||||
|
||||
return new Promise<IMessagePassingProtocol>((resolve, reject) => {
|
||||
|
||||
const socket = createConnection(pipeName, () => {
|
||||
socket.removeListener('error', reject);
|
||||
resolve(new Protocol(socket));
|
||||
});
|
||||
socket.once('error', reject);
|
||||
|
||||
}).then(protocol => {
|
||||
|
||||
return new class implements IMessagePassingProtocol {
|
||||
|
||||
private _terminating = false;
|
||||
|
||||
readonly onMessage: Event<any> = Event.filter(protocol.onMessage, msg => {
|
||||
if (!isMessageOfType(msg, MessageType.Terminate)) {
|
||||
return true;
|
||||
}
|
||||
this._terminating = true;
|
||||
onTerminate();
|
||||
return false;
|
||||
});
|
||||
|
||||
send(msg: any): void {
|
||||
if (!this._terminating) {
|
||||
protocol.send(msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function connectToRenderer(protocol: IMessagePassingProtocol): Promise<IRendererConnection> {
|
||||
return new Promise<IRendererConnection>((c, e) => {
|
||||
|
||||
// Listen init data message
|
||||
const first = protocol.onMessage(raw => {
|
||||
first.dispose();
|
||||
|
||||
const initData = <IInitData>JSON.parse(raw.toString());
|
||||
|
||||
const rendererCommit = initData.commit;
|
||||
const myCommit = product.commit;
|
||||
|
||||
if (rendererCommit && myCommit) {
|
||||
// Running in the built version where commits are defined
|
||||
if (rendererCommit !== myCommit) {
|
||||
exit(55);
|
||||
}
|
||||
}
|
||||
|
||||
// Print a console message when rejection isn't handled within N seconds. For details:
|
||||
// see https://nodejs.org/api/process.html#process_event_unhandledrejection
|
||||
// and https://nodejs.org/api/process.html#process_event_rejectionhandled
|
||||
const unhandledPromises: Promise<any>[] = [];
|
||||
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
|
||||
unhandledPromises.push(promise);
|
||||
setTimeout(() => {
|
||||
const idx = unhandledPromises.indexOf(promise);
|
||||
if (idx >= 0) {
|
||||
unhandledPromises.splice(idx, 1);
|
||||
console.warn('rejected promise not handled within 1 second');
|
||||
onUnexpectedError(reason);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
process.on('rejectionHandled', (promise: Promise<any>) => {
|
||||
const idx = unhandledPromises.indexOf(promise);
|
||||
if (idx >= 0) {
|
||||
unhandledPromises.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Print a console message when an exception isn't handled.
|
||||
process.on('uncaughtException', function (err: Error) {
|
||||
onUnexpectedError(err);
|
||||
});
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
process.on('SIGPIPE', () => {
|
||||
onUnexpectedError(new Error('Unexpected SIGPIPE'));
|
||||
});
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
|
||||
// Kill oneself if one's parent dies. Much drama.
|
||||
setInterval(function () {
|
||||
try {
|
||||
process.kill(initData.parentPid, 0); // throws an exception if the main process doesn't exist anymore.
|
||||
} catch (e) {
|
||||
onTerminate();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// In certain cases, the event loop can become busy and never yield
|
||||
// e.g. while-true or process.nextTick endless loops
|
||||
// So also use the native node module to do it from a separate thread
|
||||
let watchdog: typeof nativeWatchdog;
|
||||
try {
|
||||
watchdog = require.__$__nodeRequire('native-watchdog');
|
||||
watchdog.start(initData.parentPid);
|
||||
} catch (err) {
|
||||
// no problem...
|
||||
onUnexpectedError(err);
|
||||
}
|
||||
|
||||
// Tell the outside that we are initialized
|
||||
protocol.send(createMessageOfType(MessageType.Initialized));
|
||||
|
||||
c({ protocol, initData });
|
||||
});
|
||||
|
||||
// Tell the outside that we are ready to receive messages
|
||||
protocol.send(createMessageOfType(MessageType.Ready));
|
||||
});
|
||||
}
|
||||
|
||||
patchExecArgv();
|
||||
|
||||
createExtHostProtocol().then(protocol => {
|
||||
// connect to main side
|
||||
return connectToRenderer(protocol);
|
||||
}).then(renderer => {
|
||||
// setup things
|
||||
const extensionHostMain = new ExtensionHostMain(renderer.protocol, renderer.initData);
|
||||
onTerminate = () => extensionHostMain.terminate();
|
||||
}).catch(err => console.error(err));
|
||||
|
||||
function patchExecArgv() {
|
||||
// when encountering the prevent-inspect flag we delete this
|
||||
// and the prior flag
|
||||
if (process.env.VSCODE_PREVENT_FOREIGN_INSPECT) {
|
||||
for (let i = 0; i < process.execArgv.length; i++) {
|
||||
if (process.execArgv[i].match(/--inspect-brk=\d+|--inspect=\d+/)) {
|
||||
process.execArgv.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,28 +42,4 @@ export class ExtensionManagementServerService implements IExtensionManagementSer
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class SingleServerExtensionManagementServerService implements IExtensionManagementServerService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
|
||||
constructor(
|
||||
private readonly extensionManagementServer: IExtensionManagementServer
|
||||
) {
|
||||
}
|
||||
|
||||
getExtensionManagementServer(location: URI): IExtensionManagementServer | null {
|
||||
const authority = location.scheme === Schemas.file ? localExtensionManagementServerAuthority : location.authority;
|
||||
return this.extensionManagementServer.authority === authority ? this.extensionManagementServer : null;
|
||||
}
|
||||
|
||||
get localExtensionManagementServer(): IExtensionManagementServer | null {
|
||||
return this.extensionManagementServer.authority === localExtensionManagementServerAuthority ? this.extensionManagementServer : null;
|
||||
}
|
||||
|
||||
get remoteExtensionManagementServer(): IExtensionManagementServer | null {
|
||||
return this.extensionManagementServer.authority !== localExtensionManagementServerAuthority ? this.extensionManagementServer : null;
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@ import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { getGalleryExtensionId, getLocalExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
|
||||
import { getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { isValidExtensionVersion } from 'vs/platform/extensions/node/extensionValidator';
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionIdentifier, ExtensionIdentifierWithVersion } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
const MANIFEST_FILE = 'package.json';
|
||||
|
||||
@@ -304,6 +304,8 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
|
||||
// Relax the readonly properties here, it is the one place where we check and normalize values
|
||||
export interface IRelaxedExtensionDescription {
|
||||
id: string;
|
||||
uuid?: string;
|
||||
identifier: ExtensionIdentifier;
|
||||
name: string;
|
||||
version: string;
|
||||
publisher: string;
|
||||
@@ -343,6 +345,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler {
|
||||
|
||||
// id := `publisher.name`
|
||||
extensionDescription.id = `${extensionDescription.publisher}.${extensionDescription.name}`;
|
||||
extensionDescription.identifier = new ExtensionIdentifier(extensionDescription.id);
|
||||
|
||||
// main := absolutePath(`main`)
|
||||
if (extensionDescription.main) {
|
||||
@@ -507,7 +510,7 @@ export class ExtensionScanner {
|
||||
/**
|
||||
* Read the extension defined in `absoluteFolderPath`
|
||||
*/
|
||||
public static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): Promise<IExtensionDescription | null> {
|
||||
private static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): Promise<IExtensionDescription | null> {
|
||||
absoluteFolderPath = path.normalize(absoluteFolderPath);
|
||||
|
||||
let parser = new ExtensionManifestParser(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment);
|
||||
@@ -557,31 +560,17 @@ export class ExtensionScanner {
|
||||
refs.sort((a, b) => a.name < b.name ? -1 : 1);
|
||||
|
||||
if (!isBuiltin) {
|
||||
// TODO: align with extensionsService
|
||||
const nonGallery: IExtensionReference[] = [];
|
||||
const gallery: IExtensionReference[] = [];
|
||||
|
||||
refs.forEach(ref => {
|
||||
if (ref.name.indexOf('.') !== 0) { // Do not consider user extension folder starting with `.`
|
||||
const { id, version } = getIdAndVersionFromLocalExtensionId(ref.name);
|
||||
if (!id || !version) {
|
||||
nonGallery.push(ref);
|
||||
} else {
|
||||
gallery.push(ref);
|
||||
}
|
||||
}
|
||||
});
|
||||
refs = [...nonGallery, ...gallery];
|
||||
refs = refs.filter(ref => ref.name.indexOf('.') !== 0); // Do not consider user extension folder starting with `.`
|
||||
}
|
||||
|
||||
const nlsConfig = ExtensionScannerInput.createNLSConfig(input);
|
||||
let _extensionDescriptions = await Promise.all(refs.map(r => this.scanExtension(input.ourVersion, log, r.path, isBuiltin, isUnderDevelopment, nlsConfig)));
|
||||
let extensionDescriptions = arrays.coalesce(_extensionDescriptions);
|
||||
extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[getLocalExtensionId(getGalleryExtensionId(item.publisher, item.name), item.version)]);
|
||||
extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[new ExtensionIdentifierWithVersion({ id: getGalleryExtensionId(item.publisher, item.name) }, item.version).key()]);
|
||||
|
||||
if (!isBuiltin) {
|
||||
// Filter out outdated extensions
|
||||
const byExtension: IExtensionDescription[][] = groupByExtension(extensionDescriptions, e => ({ id: e.id, uuid: e.uuid }));
|
||||
const byExtension: IExtensionDescription[][] = groupByExtension(extensionDescriptions, e => ({ id: e.identifier.value, uuid: e.uuid }));
|
||||
extensionDescriptions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]);
|
||||
}
|
||||
|
||||
@@ -624,15 +613,23 @@ export class ExtensionScanner {
|
||||
});
|
||||
}
|
||||
|
||||
public static scanSingleExtension(input: ExtensionScannerInput, log: ILog): Promise<IExtensionDescription | null> {
|
||||
const absoluteFolderPath = input.absoluteFolderPath;
|
||||
const isBuiltin = input.isBuiltin;
|
||||
const isUnderDevelopment = input.isUnderDevelopment;
|
||||
const nlsConfig = ExtensionScannerInput.createNLSConfig(input);
|
||||
return this.scanExtension(input.ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig);
|
||||
}
|
||||
|
||||
public static mergeBuiltinExtensions(builtinExtensions: Promise<IExtensionDescription[]>, extraBuiltinExtensions: Promise<IExtensionDescription[]>): Promise<IExtensionDescription[]> {
|
||||
return Promise.all([builtinExtensions, extraBuiltinExtensions]).then(([builtinExtensions, extraBuiltinExtensions]) => {
|
||||
let resultMap: { [id: string]: IExtensionDescription; } = Object.create(null);
|
||||
for (let i = 0, len = builtinExtensions.length; i < len; i++) {
|
||||
resultMap[builtinExtensions[i].id] = builtinExtensions[i];
|
||||
resultMap[ExtensionIdentifier.toKey(builtinExtensions[i].identifier)] = builtinExtensions[i];
|
||||
}
|
||||
// Overwrite with extensions found in extra
|
||||
for (let i = 0, len = extraBuiltinExtensions.length; i < len; i++) {
|
||||
resultMap[extraBuiltinExtensions[i].id] = extraBuiltinExtensions[i];
|
||||
resultMap[ExtensionIdentifier.toKey(extraBuiltinExtensions[i].identifier)] = extraBuiltinExtensions[i];
|
||||
}
|
||||
|
||||
let resultArr = Object.keys(resultMap).map((id) => resultMap[id]);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
|
||||
export class LazyPromise implements Thenable<any> {
|
||||
export class LazyPromise implements Promise<any> {
|
||||
|
||||
private _actual: Promise<any> | null;
|
||||
private _actualOk: ((value?: any) => any) | null;
|
||||
@@ -78,4 +78,12 @@ export class LazyPromise implements Thenable<any> {
|
||||
public then(success: any, error: any): any {
|
||||
return this._ensureActual().then(success, error);
|
||||
}
|
||||
|
||||
public catch(error: any): any {
|
||||
return this._ensureActual().then(undefined, error);
|
||||
}
|
||||
|
||||
public finally(callback): any {
|
||||
return this._ensureActual().finally(callback);
|
||||
}
|
||||
}
|
||||
|
||||
317
src/vs/workbench/services/extensions/node/proxyResolver.ts
Normal file
317
src/vs/workbench/services/extensions/node/proxyResolver.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as nodeurl from 'url';
|
||||
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
|
||||
import { ExtHostConfigProvider } from 'vs/workbench/api/node/extHostConfiguration';
|
||||
import { ProxyAgent } from 'vscode-proxy-agent';
|
||||
import { MainThreadTelemetryShape } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
interface ConnectionResult {
|
||||
proxy: string;
|
||||
connection: string;
|
||||
code: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function connectProxyResolver(
|
||||
extHostWorkspace: ExtHostWorkspace,
|
||||
configProvider: ExtHostConfigProvider,
|
||||
extensionService: ExtHostExtensionService,
|
||||
extHostLogService: ExtHostLogService,
|
||||
mainThreadTelemetry: MainThreadTelemetryShape
|
||||
) {
|
||||
const agents = createProxyAgents(extHostWorkspace, configProvider, extHostLogService, mainThreadTelemetry);
|
||||
const lookup = createPatchedModules(configProvider, agents);
|
||||
return configureModuleLoading(extensionService, lookup);
|
||||
}
|
||||
|
||||
const maxCacheEntries = 5000; // Cache can grow twice that much due to 'oldCache'.
|
||||
|
||||
function createProxyAgents(
|
||||
extHostWorkspace: ExtHostWorkspace,
|
||||
configProvider: ExtHostConfigProvider,
|
||||
extHostLogService: ExtHostLogService,
|
||||
mainThreadTelemetry: MainThreadTelemetryShape
|
||||
) {
|
||||
let settingsProxy = proxyFromConfigURL(configProvider.getConfiguration('http')
|
||||
.get<string>('proxy'));
|
||||
configProvider.onDidChangeConfiguration(e => {
|
||||
settingsProxy = proxyFromConfigURL(configProvider.getConfiguration('http')
|
||||
.get<string>('proxy'));
|
||||
});
|
||||
const env = process.env;
|
||||
let envProxy = proxyFromConfigURL(env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY); // Not standardized.
|
||||
|
||||
let cacheRolls = 0;
|
||||
let oldCache = new Map<string, string>();
|
||||
let cache = new Map<string, string>();
|
||||
function getCacheKey(url: nodeurl.UrlWithStringQuery) {
|
||||
// Expecting proxies to usually be the same per scheme://host:port. Assuming that for performance.
|
||||
return nodeurl.format({ ...url, ...{ pathname: undefined, search: undefined, hash: undefined } });
|
||||
}
|
||||
function getCachedProxy(key: string) {
|
||||
let proxy = cache.get(key);
|
||||
if (proxy) {
|
||||
return proxy;
|
||||
}
|
||||
proxy = oldCache.get(key);
|
||||
if (proxy) {
|
||||
oldCache.delete(key);
|
||||
cacheProxy(key, proxy);
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
function cacheProxy(key: string, proxy: string) {
|
||||
cache.set(key, proxy);
|
||||
if (cache.size >= maxCacheEntries) {
|
||||
oldCache = cache;
|
||||
cache = new Map();
|
||||
cacheRolls++;
|
||||
extHostLogService.trace('ProxyResolver#cacheProxy cacheRolls', cacheRolls);
|
||||
}
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timer | undefined;
|
||||
let count = 0;
|
||||
let duration = 0;
|
||||
let errorCount = 0;
|
||||
let cacheCount = 0;
|
||||
let envCount = 0;
|
||||
let settingsCount = 0;
|
||||
let localhostCount = 0;
|
||||
let results: ConnectionResult[] = [];
|
||||
function logEvent() {
|
||||
timeout = undefined;
|
||||
/* __GDPR__
|
||||
"resolveProxy" : {
|
||||
"count": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"errorCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"cacheCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"cacheSize": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"cacheRolls": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"envCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"settingsCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"localhostCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"results": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
|
||||
}
|
||||
*/
|
||||
mainThreadTelemetry.$publicLog('resolveProxy', { count, duration, errorCount, cacheCount, cacheSize: cache.size, cacheRolls, envCount, settingsCount, localhostCount, results });
|
||||
count = duration = errorCount = cacheCount = envCount = settingsCount = localhostCount = 0;
|
||||
results = [];
|
||||
}
|
||||
|
||||
function resolveProxy(req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) {
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(logEvent, 10 * 60 * 1000);
|
||||
}
|
||||
|
||||
const parsedUrl = nodeurl.parse(url); // Coming from Node's URL, sticking with that.
|
||||
|
||||
const hostname = parsedUrl.hostname;
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '::ffff:127.0.0.1') {
|
||||
localhostCount++;
|
||||
callback('DIRECT');
|
||||
extHostLogService.trace('ProxyResolver#resolveProxy localhost', url, 'DIRECT');
|
||||
return;
|
||||
}
|
||||
|
||||
if (settingsProxy) {
|
||||
settingsCount++;
|
||||
callback(settingsProxy);
|
||||
extHostLogService.trace('ProxyResolver#resolveProxy settings', url, settingsProxy);
|
||||
return;
|
||||
}
|
||||
|
||||
if (envProxy) {
|
||||
envCount++;
|
||||
callback(envProxy);
|
||||
extHostLogService.trace('ProxyResolver#resolveProxy env', url, envProxy);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = getCacheKey(parsedUrl);
|
||||
const proxy = getCachedProxy(key);
|
||||
if (proxy) {
|
||||
cacheCount++;
|
||||
collectResult(results, proxy, parsedUrl.protocol === 'https:' ? 'HTTPS' : 'HTTP', req);
|
||||
callback(proxy);
|
||||
extHostLogService.trace('ProxyResolver#resolveProxy cached', url, proxy);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
extHostWorkspace.resolveProxy(url) // Use full URL to ensure it is an actually used one.
|
||||
.then(proxy => {
|
||||
cacheProxy(key, proxy);
|
||||
collectResult(results, proxy, parsedUrl.protocol === 'https:' ? 'HTTPS' : 'HTTP', req);
|
||||
callback(proxy);
|
||||
extHostLogService.debug('ProxyResolver#resolveProxy', url, proxy);
|
||||
}).then(() => {
|
||||
count++;
|
||||
duration = Date.now() - start + duration;
|
||||
}, err => {
|
||||
errorCount++;
|
||||
callback();
|
||||
extHostLogService.error('ProxyResolver#resolveProxy', toErrorMessage(err));
|
||||
});
|
||||
}
|
||||
|
||||
const httpAgent: http.Agent = new ProxyAgent({ resolveProxy });
|
||||
(<any>httpAgent).defaultPort = 80;
|
||||
const httpsAgent: http.Agent = new ProxyAgent({ resolveProxy });
|
||||
(<any>httpsAgent).defaultPort = 443;
|
||||
return { http: httpAgent, https: httpsAgent };
|
||||
}
|
||||
|
||||
function collectResult(results: ConnectionResult[], resolveProxy: string, connection: string, req: http.ClientRequest) {
|
||||
const proxy = resolveProxy ? String(resolveProxy).trim().split(/\s+/, 1)[0] : 'EMPTY';
|
||||
req.on('response', res => {
|
||||
const code = `HTTP_${res.statusCode}`;
|
||||
const result = findOrCreateResult(results, proxy, connection, code);
|
||||
result.count++;
|
||||
});
|
||||
req.on('error', err => {
|
||||
const code = err && typeof (<any>err).code === 'string' && (<any>err).code || 'UNKNOWN_ERROR';
|
||||
const result = findOrCreateResult(results, proxy, connection, code);
|
||||
result.count++;
|
||||
});
|
||||
}
|
||||
|
||||
function findOrCreateResult(results: ConnectionResult[], proxy: string, connection: string, code: string): ConnectionResult | undefined {
|
||||
for (const result of results) {
|
||||
if (result.proxy === proxy && result.connection === connection && result.code === code) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
const result = { proxy, connection, code, count: 0 };
|
||||
results.push(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function proxyFromConfigURL(configURL: string) {
|
||||
const url = (configURL || '').trim();
|
||||
const i = url.indexOf('://');
|
||||
if (i === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const scheme = url.substr(0, i).toLowerCase();
|
||||
const proxy = url.substr(i + 3);
|
||||
if (scheme === 'http') {
|
||||
return 'PROXY ' + proxy;
|
||||
} else if (scheme === 'https') {
|
||||
return 'HTTPS ' + proxy;
|
||||
} else if (scheme === 'socks') {
|
||||
return 'SOCKS ' + proxy;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createPatchedModules(configProvider: ExtHostConfigProvider, agents: { http: http.Agent; https: http.Agent; }) {
|
||||
const setting = {
|
||||
config: configProvider.getConfiguration('http')
|
||||
.get<string>('proxySupport') || 'off'
|
||||
};
|
||||
configProvider.onDidChangeConfiguration(e => {
|
||||
setting.config = configProvider.getConfiguration('http')
|
||||
.get<string>('proxySupport') || 'off';
|
||||
});
|
||||
|
||||
return {
|
||||
http: {
|
||||
off: assign({}, http, patches(http, agents.http, agents.https, { config: 'off' }, true)),
|
||||
on: assign({}, http, patches(http, agents.http, agents.https, { config: 'on' }, true)),
|
||||
override: assign({}, http, patches(http, agents.http, agents.https, { config: 'override' }, true)),
|
||||
onRequest: assign({}, http, patches(http, agents.http, agents.https, setting, true)),
|
||||
default: assign(http, patches(http, agents.http, agents.https, setting, false)) // run last
|
||||
},
|
||||
https: {
|
||||
off: assign({}, https, patches(https, agents.https, agents.http, { config: 'off' }, true)),
|
||||
on: assign({}, https, patches(https, agents.https, agents.http, { config: 'on' }, true)),
|
||||
override: assign({}, https, patches(https, agents.https, agents.http, { config: 'override' }, true)),
|
||||
onRequest: assign({}, https, patches(https, agents.https, agents.http, setting, true)),
|
||||
default: assign(https, patches(https, agents.https, agents.http, setting, false)) // run last
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function patches(originals: typeof http | typeof https, agent: http.Agent, otherAgent: http.Agent, setting: { config: string; }, onRequest: boolean) {
|
||||
return {
|
||||
get: patch(originals.get),
|
||||
request: patch(originals.request)
|
||||
};
|
||||
|
||||
function patch(original: typeof http.get) {
|
||||
function patched(url: string | URL, options?: http.RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest {
|
||||
if (typeof url !== 'string' && !(url && (<any>url).searchParams)) {
|
||||
callback = <any>options;
|
||||
options = url;
|
||||
url = null;
|
||||
}
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = null;
|
||||
}
|
||||
options = options || {};
|
||||
|
||||
const config = onRequest && ((<any>options)._vscodeProxySupport || /* LS */ (<any>options)._vscodeSystemProxy) || setting.config;
|
||||
if (config === 'off') {
|
||||
return original.apply(null, arguments as unknown as any[]);
|
||||
}
|
||||
|
||||
if (!options.socketPath && (config === 'override' || config === 'on' && !options.agent) && options.agent !== agent && options.agent !== otherAgent) {
|
||||
if (url) {
|
||||
const parsed = typeof url === 'string' ? new nodeurl.URL(url) : url;
|
||||
const urlOptions = {
|
||||
protocol: parsed.protocol,
|
||||
hostname: parsed.hostname.lastIndexOf('[', 0) === 0 ? parsed.hostname.slice(1, -1) : parsed.hostname,
|
||||
port: parsed.port,
|
||||
path: `${parsed.pathname}${parsed.search}`
|
||||
};
|
||||
if (parsed.username || parsed.password) {
|
||||
options.auth = `${parsed.username}:${parsed.password}`;
|
||||
}
|
||||
options = { ...urlOptions, ...options };
|
||||
} else {
|
||||
options = { ...options };
|
||||
}
|
||||
options.agent = agent;
|
||||
return original(options, callback);
|
||||
}
|
||||
|
||||
return original.apply(null, arguments as unknown as any[]);
|
||||
}
|
||||
return patched;
|
||||
}
|
||||
}
|
||||
|
||||
function configureModuleLoading(extensionService: ExtHostExtensionService, lookup: ReturnType<typeof createPatchedModules>): Promise<void> {
|
||||
return extensionService.getExtensionPathIndex()
|
||||
.then(extensionPaths => {
|
||||
const node_module = <any>require.__$__nodeRequire('module');
|
||||
const original = node_module._load;
|
||||
node_module._load = function load(request: string, parent: any, isMain: any) {
|
||||
if (request !== 'http' && request !== 'https') {
|
||||
return original.apply(this, arguments);
|
||||
}
|
||||
|
||||
const modules = lookup[request];
|
||||
const ext = extensionPaths.findSubstr(URI.file(parent.filename).fsPath);
|
||||
if (ext && ext.enableProposedApi) {
|
||||
return modules[(<any>ext).proxySupport] || modules.onRequest;
|
||||
}
|
||||
return modules.default;
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -9,9 +9,7 @@ import { CharCode } from 'vs/base/common/charCode';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { MarshalledObject } from 'vs/base/common/marshalling';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IURITransformer } from 'vs/base/common/uriIpc';
|
||||
import { IURITransformer, transformIncomingURIs } from 'vs/base/common/uriIpc';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { LazyPromise } from 'vs/workbench/services/extensions/node/lazyPromise';
|
||||
import { IRPCProtocol, ProxyIdentifier, getStringIdentifierForProxy } from 'vs/workbench/services/extensions/node/proxyIdentifier';
|
||||
@@ -40,75 +38,6 @@ function createURIReplacer(transformer: IURITransformer | null): JSONStringifyRe
|
||||
};
|
||||
}
|
||||
|
||||
function _transformOutgoingURIs(obj: any, transformer: IURITransformer, depth: number): any {
|
||||
|
||||
if (!obj || depth > 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
if (obj instanceof URI) {
|
||||
return transformer.transformOutgoing(obj);
|
||||
}
|
||||
|
||||
// walk object (or array)
|
||||
for (let key in obj) {
|
||||
if (Object.hasOwnProperty.call(obj, key)) {
|
||||
const r = _transformOutgoingURIs(obj[key], transformer, depth + 1);
|
||||
if (r !== null) {
|
||||
obj[key] = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function transformOutgoingURIs<T>(obj: T, transformer: IURITransformer): T {
|
||||
const result = _transformOutgoingURIs(obj, transformer, 0);
|
||||
if (result === null) {
|
||||
// no change
|
||||
return obj;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function _transformIncomingURIs(obj: any, transformer: IURITransformer, depth: number): any {
|
||||
|
||||
if (!obj || depth > 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
|
||||
if ((<MarshalledObject>obj).$mid === 1) {
|
||||
return transformer.transformIncoming(obj);
|
||||
}
|
||||
|
||||
// walk object (or array)
|
||||
for (let key in obj) {
|
||||
if (Object.hasOwnProperty.call(obj, key)) {
|
||||
const r = _transformIncomingURIs(obj[key], transformer, depth + 1);
|
||||
if (r !== null) {
|
||||
obj[key] = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function transformIncomingURIs<T>(obj: T, transformer: IURITransformer): T {
|
||||
const result = _transformIncomingURIs(obj, transformer, 0);
|
||||
if (result === null) {
|
||||
// no change
|
||||
return obj;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const enum RequestInitiator {
|
||||
LocalSide = 0,
|
||||
OtherSide = 1
|
||||
@@ -354,7 +283,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol {
|
||||
}
|
||||
const callId = String(req);
|
||||
|
||||
let promise: Thenable<any>;
|
||||
let promise: Promise<any>;
|
||||
let cancel: () => void;
|
||||
if (usesCancellationToken) {
|
||||
const cancellationTokenSource = new CancellationTokenSource();
|
||||
@@ -441,7 +370,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol {
|
||||
pendingReply.resolveErr(err);
|
||||
}
|
||||
|
||||
private _invokeHandler(rpcId: number, methodName: string, args: any[]): Thenable<any> {
|
||||
private _invokeHandler(rpcId: number, methodName: string, args: any[]): Promise<any> {
|
||||
try {
|
||||
return Promise.resolve(this._doInvokeHandler(rpcId, methodName, args));
|
||||
} catch (err) {
|
||||
@@ -461,7 +390,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol {
|
||||
return method.apply(actor, args);
|
||||
}
|
||||
|
||||
private _remoteCall(rpcId: number, methodName: string, args: any[]): Thenable<any> {
|
||||
private _remoteCall(rpcId: number, methodName: string, args: any[]): Promise<any> {
|
||||
if (this._isDisposed) {
|
||||
return Promise.reject<any>(errors.canceled());
|
||||
}
|
||||
@@ -592,7 +521,7 @@ class MessageBuffer {
|
||||
return buff;
|
||||
}
|
||||
|
||||
public static sizeMixedArray(arr: (string | Buffer)[], arrLengths: number[]): number {
|
||||
public static sizeMixedArray(arr: Array<string | Buffer>, arrLengths: number[]): number {
|
||||
let size = 0;
|
||||
size += 1; // arr length
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
@@ -608,7 +537,7 @@ class MessageBuffer {
|
||||
return size;
|
||||
}
|
||||
|
||||
public writeMixedArray(arr: (string | Buffer)[], arrLengths: number[]): void {
|
||||
public writeMixedArray(arr: Array<string | Buffer>, arrLengths: number[]): void {
|
||||
this._buff.writeUInt8(arr.length, this._offset, true); this._offset += 1;
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
const el = arr[i];
|
||||
@@ -623,9 +552,9 @@ class MessageBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
public readMixedArray(): (string | Buffer)[] {
|
||||
public readMixedArray(): Array<string | Buffer> {
|
||||
const arrLen = this._buff.readUInt8(this._offset, true); this._offset += 1;
|
||||
let arr: (string | Buffer)[] = new Array(arrLen);
|
||||
let arr: Array<string | Buffer> = new Array(arrLen);
|
||||
for (let i = 0; i < arrLen; i++) {
|
||||
const argType = <ArgType>this.readUInt8();
|
||||
if (argType === ArgType.String) {
|
||||
@@ -651,7 +580,7 @@ class MessageIO {
|
||||
|
||||
public static serializeRequest(req: number, rpcId: number, method: string, args: any[], usesCancellationToken: boolean, replacer: JSONStringifyReplacer | null): Buffer {
|
||||
if (this._arrayContainsBuffer(args)) {
|
||||
let massagedArgs: (string | Buffer)[] = new Array(args.length);
|
||||
let massagedArgs: Array<string | Buffer> = new Array(args.length);
|
||||
let argsLengths: number[] = new Array(args.length);
|
||||
for (let i = 0, len = args.length; i < len; i++) {
|
||||
const arg = args[i];
|
||||
@@ -695,7 +624,7 @@ class MessageIO {
|
||||
};
|
||||
}
|
||||
|
||||
private static _requestMixedArgs(req: number, rpcId: number, method: string, args: (string | Buffer)[], argsLengths: number[], usesCancellationToken: boolean): Buffer {
|
||||
private static _requestMixedArgs(req: number, rpcId: number, method: string, args: Array<string | Buffer>, argsLengths: number[], usesCancellationToken: boolean): Buffer {
|
||||
const methodByteLength = Buffer.byteLength(method, 'utf8');
|
||||
|
||||
let len = 0;
|
||||
|
||||
@@ -15,7 +15,7 @@ suite('RPCProtocol', () => {
|
||||
class MessagePassingProtocol implements IMessagePassingProtocol {
|
||||
private _pair: MessagePassingProtocol;
|
||||
|
||||
private readonly _onMessage: Emitter<Buffer> = new Emitter<Buffer>();
|
||||
private readonly _onMessage = new Emitter<Buffer>();
|
||||
public readonly onMessage: Event<Buffer> = this._onMessage.event;
|
||||
|
||||
public setPair(other: MessagePassingProtocol) {
|
||||
@@ -32,7 +32,7 @@ suite('RPCProtocol', () => {
|
||||
let delegate: (a1: any, a2: any) => any;
|
||||
let bProxy: BClass;
|
||||
class BClass {
|
||||
$m(a1: any, a2: any): Thenable<any> {
|
||||
$m(a1: any, a2: any): Promise<any> {
|
||||
return Promise.resolve(delegate.call(null, a1, a2));
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,6 @@ suite('RPCProtocol', () => {
|
||||
let A = new RPCProtocol(a_protocol);
|
||||
let B = new RPCProtocol(b_protocol);
|
||||
|
||||
delegate = null;
|
||||
|
||||
const bIdentifier = new ProxyIdentifier<BClass>(false, 'bb');
|
||||
const bInstance = new BClass();
|
||||
B.set(bIdentifier, bInstance);
|
||||
|
||||
Reference in New Issue
Block a user