mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Merge from master
This commit is contained in:
@@ -2,14 +2,12 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
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 { Event } from 'vs/base/common/event';
|
||||
import URI from 'vs/base/common/uri';
|
||||
|
||||
export interface IExtensionDescription {
|
||||
readonly id: string;
|
||||
@@ -37,6 +35,17 @@ export interface IExtensionDescription {
|
||||
enableProposedApi?: boolean;
|
||||
}
|
||||
|
||||
export const nullExtensionDescription = Object.freeze(<IExtensionDescription>{
|
||||
id: 'nullExtensionDescription',
|
||||
name: 'Null Extension Description',
|
||||
version: '0.0.0',
|
||||
publisher: 'vscode',
|
||||
enableProposedApi: false,
|
||||
engines: { vscode: '' },
|
||||
extensionLocation: URI.parse('void:location'),
|
||||
isBuiltin: false,
|
||||
});
|
||||
|
||||
export const IExtensionService = createDecorator<IExtensionService>('extensionService');
|
||||
|
||||
export interface IMessage {
|
||||
@@ -118,7 +127,19 @@ export class ExtensionPointContribution<T> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IExtensionService {
|
||||
export const ExtensionHostLogFileName = 'exthost';
|
||||
|
||||
export interface IWillActivateEvent {
|
||||
readonly event: string;
|
||||
readonly activation: Thenable<void>;
|
||||
}
|
||||
|
||||
export interface IResponsiveStateChangeEvent {
|
||||
target: ICpuProfilerTarget;
|
||||
isResponsive: boolean;
|
||||
}
|
||||
|
||||
export interface IExtensionService extends ICpuProfilerTarget {
|
||||
_serviceBrand: any;
|
||||
|
||||
/**
|
||||
@@ -137,26 +158,43 @@ export interface IExtensionService {
|
||||
*/
|
||||
onDidChangeExtensionsStatus: Event<string[]>;
|
||||
|
||||
/**
|
||||
* An event that is fired when activation happens.
|
||||
*/
|
||||
onWillActivateByEvent: Event<IWillActivateEvent>;
|
||||
|
||||
/**
|
||||
* An event that is fired when an extension host changes its
|
||||
* responsive-state.
|
||||
*/
|
||||
onDidChangeResponsiveChange: Event<IResponsiveStateChangeEvent>;
|
||||
|
||||
/**
|
||||
* Send an activation event and activate interested extensions.
|
||||
*/
|
||||
activateByEvent(activationEvent: string): TPromise<void>;
|
||||
activateByEvent(activationEvent: string): Thenable<void>;
|
||||
|
||||
/**
|
||||
* An promise that resolves when the installed extensions are registered after
|
||||
* their extension points got handled.
|
||||
*/
|
||||
whenInstalledExtensionsRegistered(): TPromise<boolean>;
|
||||
whenInstalledExtensionsRegistered(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Return all registered extensions
|
||||
*/
|
||||
getExtensions(): TPromise<IExtensionDescription[]>;
|
||||
getExtensions(): Promise<IExtensionDescription[]>;
|
||||
|
||||
/**
|
||||
* Return a specific extension
|
||||
* @param id An extension id
|
||||
*/
|
||||
getExtension(id: string): Promise<IExtensionDescription | undefined>;
|
||||
|
||||
/**
|
||||
* Read all contributions to an extension point.
|
||||
*/
|
||||
readExtensionPointContributions<T>(extPoint: IExtensionPoint<T>): TPromise<ExtensionPointContribution<T>[]>;
|
||||
readExtensionPointContributions<T>(extPoint: IExtensionPoint<T>): Promise<ExtensionPointContribution<T>[]>;
|
||||
|
||||
/**
|
||||
* Get information about extensions status.
|
||||
@@ -164,14 +202,9 @@ export interface IExtensionService {
|
||||
getExtensionsStatus(): { [id: string]: IExtensionsStatus };
|
||||
|
||||
/**
|
||||
* Check if the extension host can be profiled.
|
||||
* Return the inspect port or 0.
|
||||
*/
|
||||
canProfileExtensionHost(): boolean;
|
||||
|
||||
/**
|
||||
* Begin an extension host process profile session.
|
||||
*/
|
||||
startExtensionHostProfile(): TPromise<ProfileSession>;
|
||||
getInspectPort(): number;
|
||||
|
||||
/**
|
||||
* Restarts the extension host.
|
||||
@@ -189,6 +222,29 @@ export interface IExtensionService {
|
||||
stopExtensionHost(): void;
|
||||
}
|
||||
|
||||
export interface ProfileSession {
|
||||
stop(): TPromise<IExtensionHostProfile>;
|
||||
export interface ICpuProfilerTarget {
|
||||
|
||||
/**
|
||||
* Check if the extension host can be profiled.
|
||||
*/
|
||||
canProfileExtensionHost(): boolean;
|
||||
|
||||
/**
|
||||
* Begin an extension host process profile session.
|
||||
*/
|
||||
startExtensionHostProfile(): Promise<ProfileSession>;
|
||||
}
|
||||
|
||||
export interface ProfileSession {
|
||||
stop(): Promise<IExtensionHostProfile>;
|
||||
}
|
||||
|
||||
export function checkProposedApiEnabled(extension: IExtensionDescription): void {
|
||||
if (!extension.enableProposedApi) {
|
||||
throwProposedApiError(extension);
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { IMessage, IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IExtensionDescription, IMessage } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
const schemaRegistry = Registry.as<IJSONContributionRegistry>(Extensions.JSONContribution);
|
||||
@@ -72,8 +71,8 @@ export interface IExtensionPoint<T> {
|
||||
export class ExtensionPoint<T> implements IExtensionPoint<T> {
|
||||
|
||||
public readonly name: string;
|
||||
private _handler: IExtensionPointHandler<T>;
|
||||
private _users: IExtensionPointUser<T>[];
|
||||
private _handler: IExtensionPointHandler<T> | null;
|
||||
private _users: IExtensionPointUser<T>[] | null;
|
||||
private _done: boolean;
|
||||
|
||||
constructor(name: string) {
|
||||
@@ -120,7 +119,7 @@ export class ExtensionPoint<T> implements IExtensionPoint<T> {
|
||||
}
|
||||
|
||||
const schemaId = 'vscode://schemas/vscode-extensions';
|
||||
const schema: IJSONSchema = {
|
||||
export const schema = {
|
||||
properties: {
|
||||
engines: {
|
||||
type: 'object',
|
||||
@@ -215,6 +214,11 @@ const schema: IJSONSchema = {
|
||||
description: nls.localize('vscode.extension.activationEvents.onDebugResolve', 'An activation event emitted whenever a debug session with the specific type is about to be launched (and a corresponding resolveDebugConfiguration method needs to be called).'),
|
||||
body: 'onDebugResolve:${6:type}'
|
||||
},
|
||||
{
|
||||
label: 'onDebugAdapterProtocolTracker',
|
||||
description: nls.localize('vscode.extension.activationEvents.onDebugAdapterProtocolTracker', 'An activation event emitted whenever a debug session with the specific type is about to be launched and a debug protocol tracker might be needed.'),
|
||||
body: 'onDebugAdapterProtocolTracker:${6:type}'
|
||||
},
|
||||
{
|
||||
label: 'workspaceContains',
|
||||
description: nls.localize('vscode.extension.activationEvents.workspaceContains', 'An activation event emitted whenever a folder is opened that contains at least a file matching the specified glob pattern.'),
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { fsPath } from 'vs/base/common/resources';
|
||||
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 pkg from 'vs/platform/node/package';
|
||||
import product from 'vs/platform/node/product';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionScanner, ExtensionScannerInput, IExtensionReference, IExtensionResolver, ILog, IRelaxedExtensionDescription, Translations } from 'vs/workbench/services/extensions/node/extensionPoints';
|
||||
|
||||
interface IExtensionCacheData {
|
||||
input: ExtensionScannerInput;
|
||||
result: IExtensionDescription[];
|
||||
}
|
||||
|
||||
let _SystemExtensionsRoot: string | null = null;
|
||||
function getSystemExtensionsRoot(): string {
|
||||
if (!_SystemExtensionsRoot) {
|
||||
_SystemExtensionsRoot = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'extensions'));
|
||||
}
|
||||
return _SystemExtensionsRoot;
|
||||
}
|
||||
|
||||
let _ExtraDevSystemExtensionsRoot: string | null = null;
|
||||
function getExtraDevSystemExtensionsRoot(): string {
|
||||
if (!_ExtraDevSystemExtensionsRoot) {
|
||||
_ExtraDevSystemExtensionsRoot = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', '.build', 'builtInExtensions'));
|
||||
}
|
||||
return _ExtraDevSystemExtensionsRoot;
|
||||
}
|
||||
|
||||
export class CachedExtensionScanner {
|
||||
|
||||
public readonly scannedExtensions: Promise<IExtensionDescription[]>;
|
||||
private _scannedExtensionsResolve: (result: IExtensionDescription[]) => void;
|
||||
private _scannedExtensionsReject: (err: any) => void;
|
||||
|
||||
constructor(
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
@IExtensionEnablementService private readonly _extensionEnablementService: IExtensionEnablementService,
|
||||
@IWindowService private readonly _windowService: IWindowService,
|
||||
) {
|
||||
this.scannedExtensions = new Promise<IExtensionDescription[]>((resolve, reject) => {
|
||||
this._scannedExtensionsResolve = resolve;
|
||||
this._scannedExtensionsReject = reject;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static async _validateExtensionsCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): Promise<void> {
|
||||
const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER);
|
||||
const cacheFile = path.join(cacheFolder, cacheKey);
|
||||
|
||||
const expected = JSON.parse(JSON.stringify(await ExtensionScanner.scanExtensions(input, new NullLogger())));
|
||||
|
||||
const cacheContents = await this._readExtensionCache(environmentService, cacheKey);
|
||||
if (!cacheContents) {
|
||||
// Cache has been deleted by someone else, which is perfectly fine...
|
||||
return;
|
||||
}
|
||||
const actual = cacheContents.result;
|
||||
|
||||
if (objects.equals(expected, actual)) {
|
||||
// Cache is valid and running with it is perfectly fine...
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await pfs.del(cacheFile);
|
||||
} catch (err) {
|
||||
errors.onUnexpectedError(err);
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
notificationService.prompt(
|
||||
Severity.Error,
|
||||
nls.localize('extensionCache.invalid', "Extensions have been modified on disk. Please reload the window."),
|
||||
[{
|
||||
label: nls.localize('reloadWindow', "Reload Window"),
|
||||
run: () => windowService.reloadWindow()
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
private static async _readExtensionCache(environmentService: IEnvironmentService, cacheKey: string): Promise<IExtensionCacheData | null> {
|
||||
const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER);
|
||||
const cacheFile = path.join(cacheFolder, cacheKey);
|
||||
|
||||
try {
|
||||
const cacheRawContents = await pfs.readFile(cacheFile, 'utf8');
|
||||
return JSON.parse(cacheRawContents);
|
||||
} catch (err) {
|
||||
// That's ok...
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async _writeExtensionCache(environmentService: IEnvironmentService, cacheKey: string, cacheContents: IExtensionCacheData): Promise<void> {
|
||||
const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER);
|
||||
const cacheFile = path.join(cacheFolder, cacheKey);
|
||||
|
||||
try {
|
||||
await pfs.mkdirp(cacheFolder);
|
||||
} catch (err) {
|
||||
// That's ok...
|
||||
}
|
||||
|
||||
try {
|
||||
await pfs.writeFile(cacheFile, JSON.stringify(cacheContents));
|
||||
} catch (err) {
|
||||
// That's ok...
|
||||
}
|
||||
}
|
||||
|
||||
private static async _scanExtensionsWithCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput, log: ILog): Promise<IExtensionDescription[]> {
|
||||
if (input.devMode) {
|
||||
// Do not cache when running out of sources...
|
||||
return ExtensionScanner.scanExtensions(input, log);
|
||||
}
|
||||
|
||||
try {
|
||||
const folderStat = await pfs.stat(input.absoluteFolderPath);
|
||||
input.mtime = folderStat.mtime.getTime();
|
||||
} catch (err) {
|
||||
// That's ok...
|
||||
}
|
||||
|
||||
const cacheContents = await this._readExtensionCache(environmentService, cacheKey);
|
||||
if (cacheContents && cacheContents.input && ExtensionScannerInput.equals(cacheContents.input, input)) {
|
||||
// Validate the cache asynchronously after 5s
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this._validateExtensionsCache(windowService, notificationService, environmentService, cacheKey, input);
|
||||
} catch (err) {
|
||||
errors.onUnexpectedError(err);
|
||||
}
|
||||
}, 5000);
|
||||
return cacheContents.result.map((extensionDescription) => {
|
||||
// revive URI object
|
||||
(<IRelaxedExtensionDescription>extensionDescription).extensionLocation = URI.revive(extensionDescription.extensionLocation);
|
||||
return extensionDescription;
|
||||
});
|
||||
}
|
||||
|
||||
const counterLogger = new CounterLogger(log);
|
||||
const result = await ExtensionScanner.scanExtensions(input, counterLogger);
|
||||
if (counterLogger.errorCnt === 0) {
|
||||
// Nothing bad happened => cache the result
|
||||
const cacheContents: IExtensionCacheData = {
|
||||
input: input,
|
||||
result: result
|
||||
};
|
||||
await this._writeExtensionCache(environmentService, cacheKey, cacheContents);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
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: [] };
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
interface IBuiltInExtension {
|
||||
name: string;
|
||||
version: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
interface IBuiltInExtensionControl {
|
||||
[name: string]: 'marketplace' | 'disabled' | string;
|
||||
}
|
||||
|
||||
class ExtraBuiltInExtensionResolver implements IExtensionResolver {
|
||||
|
||||
constructor(private builtInExtensions: IBuiltInExtension[], private control: IBuiltInExtensionControl) { }
|
||||
|
||||
resolveExtensions(): Promise<IExtensionReference[]> {
|
||||
const result: IExtensionReference[] = [];
|
||||
|
||||
for (const ext of this.builtInExtensions) {
|
||||
const controlState = this.control[ext.name] || 'marketplace';
|
||||
|
||||
switch (controlState) {
|
||||
case 'disabled':
|
||||
break;
|
||||
case 'marketplace':
|
||||
result.push({ name: ext.name, path: path.join(getExtraDevSystemExtensionsRoot(), ext.name) });
|
||||
break;
|
||||
default:
|
||||
result.push({ name: ext.name, path: controlState });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
}
|
||||
|
||||
class CounterLogger implements ILog {
|
||||
|
||||
public errorCnt = 0;
|
||||
public warnCnt = 0;
|
||||
public infoCnt = 0;
|
||||
|
||||
constructor(private readonly _actual: ILog) {
|
||||
}
|
||||
|
||||
public error(source: string, message: string): void {
|
||||
this._actual.error(source, message);
|
||||
}
|
||||
|
||||
public warn(source: string, message: string): void {
|
||||
this._actual.warn(source, message);
|
||||
}
|
||||
|
||||
public info(source: string, message: string): void {
|
||||
this._actual.info(source, message);
|
||||
}
|
||||
}
|
||||
|
||||
class NullLogger implements ILog {
|
||||
public error(source: string, message: string): void {
|
||||
}
|
||||
public warn(source: string, message: string): void {
|
||||
}
|
||||
public info(source: string, message: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
export class Logger implements ILog {
|
||||
|
||||
private readonly _messageHandler: (severity: Severity, source: string, message: string) => void;
|
||||
|
||||
constructor(
|
||||
messageHandler: (severity: Severity, source: string, message: string) => void
|
||||
) {
|
||||
this._messageHandler = messageHandler;
|
||||
}
|
||||
|
||||
public error(source: string, message: string): void {
|
||||
this._messageHandler(Severity.Error, source, message);
|
||||
}
|
||||
|
||||
public warn(source: string, message: string): void {
|
||||
this._messageHandler(Severity.Warning, source, message);
|
||||
}
|
||||
|
||||
public info(source: string, message: string): void {
|
||||
this._messageHandler(Severity.Info, source, message);
|
||||
}
|
||||
}
|
||||
@@ -3,41 +3,73 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { isWindows, isLinux } from 'vs/base/common/platform';
|
||||
import { findFreePort } from 'vs/base/node/ports';
|
||||
import { ILifecycleService, ShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ChildProcess, fork } from 'child_process';
|
||||
import { ipcRenderer as ipc } from 'electron';
|
||||
import product from 'vs/platform/node/product';
|
||||
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 { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
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 { 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 { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { generateRandomPipeName, Protocol } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { createServer, Server, Socket } from 'net';
|
||||
import { Event, Emitter, debounceEvent, mapEvent, anyEvent, fromNodeEventEmitter } from 'vs/base/common/event';
|
||||
import { IInitData, IWorkspaceData, IConfigurationInitData } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
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';
|
||||
import { ILifecycleService, WillShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
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 { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IConfigurationInitData, 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 { IBroadcastService, IBroadcast } from 'vs/platform/broadcast/electron-browser/broadcastService';
|
||||
import { isEqual } from 'vs/base/common/paths';
|
||||
import { EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost';
|
||||
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IRemoteConsoleLog, log, parse } from 'vs/base/node/console';
|
||||
import { getScopes } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
export class ExtensionHostProcessWorker {
|
||||
export interface IExtensionHostStarter {
|
||||
readonly onCrashed: Event<[number, string]>;
|
||||
start(): Thenable<IMessagePassingProtocol>;
|
||||
getInspectPort(): number;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface IExtensionDevOptions {
|
||||
readonly isExtensionDevHost: boolean;
|
||||
readonly isExtensionDevDebug: boolean;
|
||||
readonly isExtensionDevDebugBrk: boolean;
|
||||
readonly isExtensionDevTestFromCli: boolean;
|
||||
}
|
||||
export function parseExtensionDevOptions(environmentService: IEnvironmentService): IExtensionDevOptions {
|
||||
// handle extension host lifecycle a bit special when we know we are developing an extension that runs inside
|
||||
let isExtensionDevHost = environmentService.isExtensionDevelopment;
|
||||
const extDevLoc = environmentService.extensionDevelopmentLocationURI;
|
||||
const debugOk = !extDevLoc || extDevLoc.scheme === Schemas.file;
|
||||
let isExtensionDevDebug = debugOk && typeof environmentService.debugExtensionHost.port === 'number';
|
||||
let isExtensionDevDebugBrk = debugOk && !!environmentService.debugExtensionHost.break;
|
||||
let isExtensionDevTestFromCli = isExtensionDevHost && !!environmentService.extensionTestsPath && !environmentService.debugExtensionHost.break;
|
||||
return {
|
||||
isExtensionDevHost,
|
||||
isExtensionDevDebug,
|
||||
isExtensionDevDebugBrk,
|
||||
isExtensionDevTestFromCli,
|
||||
};
|
||||
}
|
||||
|
||||
export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
|
||||
private readonly _onCrashed: Emitter<[number, string]> = new Emitter<[number, string]>();
|
||||
public readonly onCrashed: Event<[number, string]> = this._onCrashed.event;
|
||||
@@ -58,10 +90,12 @@ export class ExtensionHostProcessWorker {
|
||||
private _inspectPort: number;
|
||||
private _extensionHostProcess: ChildProcess;
|
||||
private _extensionHostConnection: Socket;
|
||||
private _messageProtocol: TPromise<IMessagePassingProtocol>;
|
||||
private _messageProtocol: Promise<IMessagePassingProtocol>;
|
||||
|
||||
constructor(
|
||||
private readonly _extensions: TPromise<IExtensionDescription[]>,
|
||||
private readonly _autoStart: boolean,
|
||||
private readonly _extensions: Promise<IExtensionDescription[]>,
|
||||
private readonly _extensionHostLogsLocation: URI,
|
||||
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IWindowsService private readonly _windowsService: IWindowsService,
|
||||
@@ -72,13 +106,14 @@ export class ExtensionHostProcessWorker {
|
||||
@IWorkspaceConfigurationService private readonly _configurationService: IWorkspaceConfigurationService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@ICrashReporterService private readonly _crashReporterService: ICrashReporterService,
|
||||
@ILogService private readonly _logService: ILogService
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ILabelService private readonly _labelService: ILabelService
|
||||
) {
|
||||
// handle extension host lifecycle a bit special when we know we are developing an extension that runs inside
|
||||
this._isExtensionDevHost = this._environmentService.isExtensionDevelopment;
|
||||
this._isExtensionDevDebug = (typeof this._environmentService.debugExtensionHost.port === 'number');
|
||||
this._isExtensionDevDebugBrk = !!this._environmentService.debugExtensionHost.break;
|
||||
this._isExtensionDevTestFromCli = this._isExtensionDevHost && !!this._environmentService.extensionTestsPath && !this._environmentService.debugExtensionHost.break;
|
||||
const devOpts = parseExtensionDevOptions(this._environmentService);
|
||||
this._isExtensionDevHost = devOpts.isExtensionDevHost;
|
||||
this._isExtensionDevDebug = devOpts.isExtensionDevDebug;
|
||||
this._isExtensionDevDebugBrk = devOpts.isExtensionDevDebugBrk;
|
||||
this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli;
|
||||
|
||||
this._lastExtensionHostError = null;
|
||||
this._terminating = false;
|
||||
@@ -90,7 +125,7 @@ export class ExtensionHostProcessWorker {
|
||||
|
||||
this._toDispose = [];
|
||||
this._toDispose.push(this._onCrashed);
|
||||
this._toDispose.push(this._lifecycleService.onWillShutdown((e) => this._onWillShutdown(e)));
|
||||
this._toDispose.push(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e)));
|
||||
this._toDispose.push(this._lifecycleService.onShutdown(reason => this.terminate()));
|
||||
this._toDispose.push(this._broadcastService.onBroadcast(b => this._onBroadcast(b)));
|
||||
|
||||
@@ -114,28 +149,28 @@ export class ExtensionHostProcessWorker {
|
||||
|
||||
// Close Ext Host Window Request
|
||||
if (broadcast.channel === EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL && this._isExtensionDevHost) {
|
||||
const extensionPaths = broadcast.payload as string[];
|
||||
if (Array.isArray(extensionPaths) && extensionPaths.some(path => isEqual(this._environmentService.extensionDevelopmentPath, path, !isLinux))) {
|
||||
const extensionLocations = broadcast.payload as string[];
|
||||
if (Array.isArray(extensionLocations) && extensionLocations.some(uriString => isEqual(this._environmentService.extensionDevelopmentLocationURI, URI.parse(uriString)))) {
|
||||
this._windowService.closeWindow();
|
||||
}
|
||||
}
|
||||
|
||||
if (broadcast.channel === EXTENSION_RELOAD_BROADCAST_CHANNEL && this._isExtensionDevHost) {
|
||||
const extensionPaths = broadcast.payload as string[];
|
||||
if (Array.isArray(extensionPaths) && extensionPaths.some(path => isEqual(this._environmentService.extensionDevelopmentPath, path, !isLinux))) {
|
||||
if (Array.isArray(extensionPaths) && extensionPaths.some(uriString => isEqual(this._environmentService.extensionDevelopmentLocationURI, URI.parse(uriString)))) {
|
||||
this._windowService.reloadWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public start(): TPromise<IMessagePassingProtocol> {
|
||||
public start(): Promise<IMessagePassingProtocol> {
|
||||
if (this._terminating) {
|
||||
// .terminate() was called
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this._messageProtocol) {
|
||||
this._messageProtocol = TPromise.join([this._tryListenOnPipe(), this._tryFindDebugPort()]).then(data => {
|
||||
this._messageProtocol = Promise.all([this._tryListenOnPipe(), this._tryFindDebugPort()]).then(data => {
|
||||
const pipeName = data[0];
|
||||
const portData = data[1];
|
||||
|
||||
@@ -176,7 +211,7 @@ export class ExtensionHostProcessWorker {
|
||||
}
|
||||
|
||||
// Run Extension Host as fork of current process
|
||||
this._extensionHostProcess = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts);
|
||||
this._extensionHostProcess = fork(getPathFromAmdModule(require, 'bootstrap-fork'), ['--type=extensionHost'], opts);
|
||||
|
||||
// Catch all output coming from the extension host process
|
||||
type Output = { data: string, format: string[] };
|
||||
@@ -220,7 +255,7 @@ export class ExtensionHostProcessWorker {
|
||||
this._extensionHostProcess.on('exit', (code: number, signal: string) => this._onExtHostProcessExit(code, signal));
|
||||
|
||||
// Notify debugger that we are ready to attach to the process if we run a development extension
|
||||
if (this._isExtensionDevHost && portData.actual) {
|
||||
if (this._isExtensionDevHost && portData.actual && this._isExtensionDevDebug) {
|
||||
this._broadcastService.broadcast({
|
||||
channel: EXTENSION_ATTACH_BROADCAST_CHANNEL,
|
||||
payload: {
|
||||
@@ -232,8 +267,8 @@ export class ExtensionHostProcessWorker {
|
||||
this._inspectPort = portData.actual;
|
||||
|
||||
// Help in case we fail to start it
|
||||
let startupTimeoutHandle: number;
|
||||
if (!this._environmentService.isBuilt || this._isExtensionDevHost) {
|
||||
let startupTimeoutHandle: any;
|
||||
if (!this._environmentService.isBuilt && !this._windowService.getConfiguration().remoteAuthority || this._isExtensionDevHost) {
|
||||
startupTimeoutHandle = setTimeout(() => {
|
||||
const msg = this._isExtensionDevDebugBrk
|
||||
? nls.localize('extensionHostProcess.startupFailDebug', "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.")
|
||||
@@ -243,7 +278,8 @@ export class ExtensionHostProcessWorker {
|
||||
[{
|
||||
label: nls.localize('reloadWindow', "Reload Window"),
|
||||
run: () => this._windowService.reloadWindow()
|
||||
}]
|
||||
}],
|
||||
{ sticky: true }
|
||||
);
|
||||
}, 10000);
|
||||
}
|
||||
@@ -262,8 +298,8 @@ export class ExtensionHostProcessWorker {
|
||||
/**
|
||||
* Start a server (`this._namedPipeServer`) that listens on a named pipe and return the named pipe name.
|
||||
*/
|
||||
private _tryListenOnPipe(): TPromise<string> {
|
||||
return new TPromise<string>((resolve, reject) => {
|
||||
private _tryListenOnPipe(): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const pipeName = generateRandomPipeName();
|
||||
|
||||
this._namedPipeServer = createServer();
|
||||
@@ -278,15 +314,13 @@ export class ExtensionHostProcessWorker {
|
||||
/**
|
||||
* Find a free port if extension host debugging is enabled.
|
||||
*/
|
||||
private _tryFindDebugPort(): TPromise<{ expected: number; actual: number }> {
|
||||
private _tryFindDebugPort(): Promise<{ expected: number; actual: number }> {
|
||||
let expected: number;
|
||||
let startPort = 9333;
|
||||
let startPort = randomPort();
|
||||
if (typeof this._environmentService.debugExtensionHost.port === 'number') {
|
||||
startPort = expected = this._environmentService.debugExtensionHost.port;
|
||||
} else {
|
||||
return TPromise.as({ expected: undefined, actual: 0 });
|
||||
}
|
||||
return new TPromise((c, e) => {
|
||||
return new Promise(resolve => {
|
||||
return findFreePort(startPort, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */).then(port => {
|
||||
if (!port) {
|
||||
console.warn('%c[Extension Host] %cCould not find a free port for debugging', 'color: blue', 'color: black');
|
||||
@@ -300,14 +334,14 @@ export class ExtensionHostProcessWorker {
|
||||
console.info(`%c[Extension Host] %cdebugger listening on port ${port}`, 'color: blue', 'color: black');
|
||||
}
|
||||
}
|
||||
return c({ expected, actual: port });
|
||||
return resolve({ expected, actual: port });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _tryExtHostHandshake(): TPromise<IMessagePassingProtocol> {
|
||||
private _tryExtHostHandshake(): Promise<IMessagePassingProtocol> {
|
||||
|
||||
return new TPromise<IMessagePassingProtocol>((resolve, reject) => {
|
||||
return new Promise<IMessagePassingProtocol>((resolve, reject) => {
|
||||
|
||||
// Wait for the extension host to connect to our named pipe
|
||||
// and wrap the socket in the message passing protocol
|
||||
@@ -329,30 +363,49 @@ export class ExtensionHostProcessWorker {
|
||||
|
||||
// 1) wait for the incoming `ready` event and send the initialization data.
|
||||
// 2) wait for the incoming `initialized` event.
|
||||
return new TPromise<IMessagePassingProtocol>((resolve, reject) => {
|
||||
return new Promise<IMessagePassingProtocol>((resolve, reject) => {
|
||||
|
||||
let handle = setTimeout(() => {
|
||||
reject('timeout');
|
||||
}, 60 * 1000);
|
||||
let timeoutHandle: NodeJS.Timer;
|
||||
const installTimeoutCheck = () => {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
reject('timeout');
|
||||
}, 60 * 1000);
|
||||
};
|
||||
const uninstallTimeoutCheck = () => {
|
||||
clearTimeout(timeoutHandle);
|
||||
};
|
||||
|
||||
// Wait 60s for the ready message
|
||||
installTimeoutCheck();
|
||||
|
||||
const disposable = protocol.onMessage(msg => {
|
||||
|
||||
if (msg === 'ready') {
|
||||
if (isMessageOfType(msg, MessageType.Ready)) {
|
||||
// 1) Extension Host is ready to receive messages, initialize it
|
||||
this._createExtHostInitData().then(data => protocol.send(JSON.stringify(data)));
|
||||
uninstallTimeoutCheck();
|
||||
|
||||
this._createExtHostInitData().then(data => {
|
||||
|
||||
// Wait 60s for the initialized message
|
||||
installTimeoutCheck();
|
||||
|
||||
protocol.send(Buffer.from(JSON.stringify(data)));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg === 'initialized') {
|
||||
if (isMessageOfType(msg, MessageType.Initialized)) {
|
||||
// 2) Extension Host is initialized
|
||||
|
||||
clearTimeout(handle);
|
||||
uninstallTimeoutCheck();
|
||||
|
||||
// stop listening for messages here
|
||||
disposable.dispose();
|
||||
|
||||
// release this promise
|
||||
resolve(protocol);
|
||||
// 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));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -364,29 +417,38 @@ export class ExtensionHostProcessWorker {
|
||||
});
|
||||
}
|
||||
|
||||
private _createExtHostInitData(): TPromise<IInitData> {
|
||||
return TPromise.join([this._telemetryService.getTelemetryInfo(), this._extensions]).then(([telemetryInfo, extensionDescriptions]) => {
|
||||
const configurationData: IConfigurationInitData = { ...this._configurationService.getConfigurationData(), configurationScopes: {} };
|
||||
const r: IInitData = {
|
||||
parentPid: process.pid,
|
||||
environment: {
|
||||
isExtensionDevelopmentDebug: this._isExtensionDevDebug,
|
||||
appRoot: this._environmentService.appRoot,
|
||||
appSettingsHome: this._environmentService.appSettingsHome,
|
||||
extensionDevelopmentPath: this._environmentService.extensionDevelopmentPath,
|
||||
extensionTestsPath: this._environmentService.extensionTestsPath
|
||||
},
|
||||
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : <IWorkspaceData>this._contextService.getWorkspace(),
|
||||
extensions: extensionDescriptions,
|
||||
// Send configurations scopes only in development mode.
|
||||
configuration: !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment ? { ...configurationData, configurationScopes: getScopes() } : configurationData,
|
||||
telemetryInfo,
|
||||
windowId: this._windowService.getCurrentWindowId(),
|
||||
logLevel: this._logService.getLevel(),
|
||||
logsPath: this._environmentService.logsPath
|
||||
};
|
||||
return r;
|
||||
});
|
||||
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,
|
||||
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
|
||||
extensionTestsPath: this._environmentService.extensionTestsPath,
|
||||
globalStorageHome: URI.file(this._environmentService.globalStorageHome)
|
||||
},
|
||||
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : {
|
||||
configuration: workspace.configuration,
|
||||
folders: workspace.folders,
|
||||
id: workspace.id,
|
||||
name: this._labelService.getWorkspaceLabel(workspace)
|
||||
},
|
||||
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,
|
||||
autoStart: this._autoStart
|
||||
};
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
private _logExtensionHostMessage(entry: IRemoteConsoleLog) {
|
||||
@@ -467,13 +529,11 @@ export class ExtensionHostProcessWorker {
|
||||
|
||||
// Send the extension host a request to terminate itself
|
||||
// (graceful termination)
|
||||
protocol.send({
|
||||
type: '__$terminate'
|
||||
});
|
||||
protocol.send(createMessageOfType(MessageType.Terminate));
|
||||
|
||||
// Give the extension host 60s, after which we will
|
||||
// Give the extension host 10s, after which we will
|
||||
// try to kill the process and release any resources
|
||||
setTimeout(() => this._cleanResources(), 60 * 1000);
|
||||
setTimeout(() => this._cleanResources(), 10 * 1000);
|
||||
|
||||
}, (err) => {
|
||||
|
||||
@@ -498,7 +558,7 @@ export class ExtensionHostProcessWorker {
|
||||
}
|
||||
}
|
||||
|
||||
private _onWillShutdown(event: ShutdownEvent): void {
|
||||
private _onWillShutdown(event: WillShutdownEvent): void {
|
||||
|
||||
// If the extension development host was started without debugger attached we need
|
||||
// to communicate this back to the main side to terminate the debug session
|
||||
@@ -510,7 +570,61 @@ export class ExtensionHostProcessWorker {
|
||||
}
|
||||
});
|
||||
|
||||
event.veto(TPromise.timeout(100 /* wait a bit for IPC to get delivered */).then(() => false));
|
||||
event.join(timeout(100 /* wait a bit for IPC to get delivered */));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
|
||||
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 { 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';
|
||||
|
||||
// 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);
|
||||
|
||||
export class ExtensionHostProcessManager extends Disposable {
|
||||
|
||||
public readonly onDidCrash: Event<[number, string]>;
|
||||
|
||||
private readonly _onDidChangeResponsiveState: Emitter<ResponsiveState> = this._register(new Emitter<ResponsiveState>());
|
||||
public readonly onDidChangeResponsiveState: Event<ResponsiveState> = this._onDidChangeResponsiveState.event;
|
||||
|
||||
/**
|
||||
* A map of already activated events to speed things up if the same activation event is triggered multiple times.
|
||||
*/
|
||||
private readonly _extensionHostProcessFinishedActivateEvents: { [activationEvent: string]: boolean; };
|
||||
private _extensionHostProcessRPCProtocol: RPCProtocol;
|
||||
private readonly _extensionHostProcessCustomers: IDisposable[];
|
||||
private readonly _extensionHostProcessWorker: IExtensionHostStarter;
|
||||
/**
|
||||
* 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; }>;
|
||||
|
||||
constructor(
|
||||
extensionHostProcessWorker: IExtensionHostStarter,
|
||||
private readonly _remoteAuthority: string,
|
||||
initialActivationEvents: string[],
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
) {
|
||||
super();
|
||||
this._extensionHostProcessFinishedActivateEvents = Object.create(null);
|
||||
this._extensionHostProcessRPCProtocol = null;
|
||||
this._extensionHostProcessCustomers = [];
|
||||
|
||||
this._extensionHostProcessWorker = extensionHostProcessWorker;
|
||||
this.onDidCrash = this._extensionHostProcessWorker.onCrashed;
|
||||
this._extensionHostProcessProxy = this._extensionHostProcessWorker.start().then(
|
||||
(protocol) => {
|
||||
return { value: this._createExtensionHostCustomers(protocol) };
|
||||
},
|
||||
(err) => {
|
||||
console.error('Error received from starting extension host');
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
this._extensionHostProcessProxy.then(() => {
|
||||
initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent));
|
||||
});
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._extensionHostProcessWorker) {
|
||||
this._extensionHostProcessWorker.dispose();
|
||||
}
|
||||
if (this._extensionHostProcessRPCProtocol) {
|
||||
this._extensionHostProcessRPCProtocol.dispose();
|
||||
}
|
||||
for (let i = 0, len = this._extensionHostProcessCustomers.length; i < len; i++) {
|
||||
const customer = this._extensionHostProcessCustomers[i];
|
||||
try {
|
||||
customer.dispose();
|
||||
} catch (err) {
|
||||
errors.onUnexpectedError(err);
|
||||
}
|
||||
}
|
||||
this._extensionHostProcessProxy = null;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
public getExtenstionHostProcessWorker(): IExtensionHostStarter {
|
||||
return this._extensionHostProcessWorker;
|
||||
}
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
|
||||
public canProfileExtensionHost(): boolean {
|
||||
return this._extensionHostProcessWorker && Boolean(this._extensionHostProcessWorker.getInspectPort());
|
||||
}
|
||||
|
||||
private _createExtensionHostCustomers(protocol: IMessagePassingProtocol): ExtHostExtensionServiceShape {
|
||||
|
||||
let logger: IRPCProtocolLogger | null = null;
|
||||
if (LOG_EXTENSION_HOST_COMMUNICATION || this._environmentService.logExtensionHostCommunication) {
|
||||
logger = new RPCLogger();
|
||||
}
|
||||
|
||||
this._extensionHostProcessRPCProtocol = new RPCProtocol(protocol, logger);
|
||||
this._register(this._extensionHostProcessRPCProtocol.onDidChangeResponsiveState((responsiveState: ResponsiveState) => this._onDidChangeResponsiveState.fire(responsiveState)));
|
||||
const extHostContext: IExtHostContext = {
|
||||
remoteAuthority: this._remoteAuthority,
|
||||
getProxy: <T>(identifier: ProxyIdentifier<T>): T => this._extensionHostProcessRPCProtocol.getProxy(identifier),
|
||||
set: <T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R => this._extensionHostProcessRPCProtocol.set(identifier, instance),
|
||||
assertRegistered: (identifiers: ProxyIdentifier<any>[]): void => this._extensionHostProcessRPCProtocol.assertRegistered(identifiers),
|
||||
};
|
||||
|
||||
// Named customers
|
||||
const namedCustomers = ExtHostCustomersRegistry.getNamedCustomers();
|
||||
for (let i = 0, len = namedCustomers.length; i < len; i++) {
|
||||
const [id, ctor] = namedCustomers[i];
|
||||
const instance = this._instantiationService.createInstance(ctor, extHostContext);
|
||||
this._extensionHostProcessCustomers.push(instance);
|
||||
this._extensionHostProcessRPCProtocol.set(id, instance);
|
||||
}
|
||||
|
||||
// Customers
|
||||
const customers = ExtHostCustomersRegistry.getCustomers();
|
||||
for (let i = 0, len = customers.length; i < len; i++) {
|
||||
const ctor = customers[i];
|
||||
const instance = this._instantiationService.createInstance(ctor, extHostContext);
|
||||
this._extensionHostProcessCustomers.push(instance);
|
||||
}
|
||||
|
||||
// Check that no named customers are missing
|
||||
const expected: ProxyIdentifier<any>[] = Object.keys(MainContext).map((key) => (<any>MainContext)[key]);
|
||||
this._extensionHostProcessRPCProtocol.assertRegistered(expected);
|
||||
|
||||
return this._extensionHostProcessRPCProtocol.getProxy(ExtHostContext.ExtHostExtensionService);
|
||||
}
|
||||
|
||||
public activateByEvent(activationEvent: string): Thenable<void> {
|
||||
if (this._extensionHostProcessFinishedActivateEvents[activationEvent] || !this._extensionHostProcessProxy) {
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
return this._extensionHostProcessProxy.then((proxy) => {
|
||||
if (!proxy) {
|
||||
// this case is already covered above and logged.
|
||||
// i.e. the extension host could not be started
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
return proxy.value.$activateByEvent(activationEvent);
|
||||
}).then(() => {
|
||||
this._extensionHostProcessFinishedActivateEvents[activationEvent] = true;
|
||||
});
|
||||
}
|
||||
|
||||
public startExtensionHostProfile(): Promise<ProfileSession> {
|
||||
if (this._extensionHostProcessWorker) {
|
||||
let port = this._extensionHostProcessWorker.getInspectPort();
|
||||
if (port) {
|
||||
return this._instantiationService.createInstance(ExtensionHostProfiler, port).start();
|
||||
}
|
||||
}
|
||||
throw new Error('Extension host not running or no inspect port available');
|
||||
}
|
||||
|
||||
public getInspectPort(): number {
|
||||
if (this._extensionHostProcessWorker) {
|
||||
let port = this._extensionHostProcessWorker.getInspectPort();
|
||||
if (port) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public resolveAuthority(remoteAuthority: string): Thenable<ResolvedAuthority> {
|
||||
return this._extensionHostProcessProxy.then(proxy => proxy.value.$resolveAuthority(remoteAuthority));
|
||||
}
|
||||
|
||||
public start(enabledExtensionIds: string[]): Thenable<void> {
|
||||
return this._extensionHostProcessProxy.then(proxy => proxy.value.$startExtensionHost(enabledExtensionIds));
|
||||
}
|
||||
}
|
||||
|
||||
const colorTables = [
|
||||
['#2977B1', '#FC802D', '#34A13A', '#D3282F', '#9366BA'],
|
||||
['#8B564C', '#E177C0', '#7F7F7F', '#BBBE3D', '#2EBECD']
|
||||
];
|
||||
|
||||
function prettyWithoutArrays(data: any): any {
|
||||
if (Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
if (data && typeof data === 'object' && typeof data.toString === 'function') {
|
||||
let result = data.toString();
|
||||
if (result !== '[object Object]') {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function pretty(data: any): any {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map(prettyWithoutArrays);
|
||||
}
|
||||
return prettyWithoutArrays(data);
|
||||
}
|
||||
|
||||
class RPCLogger implements IRPCProtocolLogger {
|
||||
|
||||
private _totalIncoming = 0;
|
||||
private _totalOutgoing = 0;
|
||||
|
||||
private _log(direction: string, totalLength, msgLength: number, req: number, initiator: RequestInitiator, str: string, data: any): void {
|
||||
data = pretty(data);
|
||||
|
||||
const colorTable = colorTables[initiator];
|
||||
const color = LOG_USE_COLORS ? colorTable[req % colorTable.length] : '#000000';
|
||||
let args = [`%c[${direction}]%c[${strings.pad(totalLength, 7, ' ')}]%c[len: ${strings.pad(msgLength, 5, ' ')}]%c${strings.pad(req, 5, ' ')} - ${str}`, 'color: darkgreen', 'color: grey', 'color: grey', `color: ${color}`];
|
||||
if (/\($/.test(str)) {
|
||||
args = args.concat(data);
|
||||
args.push(')');
|
||||
} else {
|
||||
args.push(data);
|
||||
}
|
||||
console.log.apply(console, args);
|
||||
}
|
||||
|
||||
logIncoming(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void {
|
||||
this._totalIncoming += msgLength;
|
||||
this._log('Ext \u2192 Win', this._totalIncoming, msgLength, req, initiator, str, data);
|
||||
}
|
||||
|
||||
logOutgoing(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void {
|
||||
this._totalOutgoing += msgLength;
|
||||
this._log('Win \u2192 Ext', this._totalOutgoing, msgLength, req, initiator, str, data);
|
||||
}
|
||||
}
|
||||
@@ -3,33 +3,26 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { IExtensionService, IExtensionDescription, ProfileSession, IExtensionHostProfile, ProfileSegmentId } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Profile, ProfileNode } from 'v8-inspect-profiler';
|
||||
import { TernarySearchTree } from 'vs/base/common/map';
|
||||
import { realpathSync } from 'vs/base/node/extfs';
|
||||
import { Profile, ProfileNode } from 'v8-inspect-profiler';
|
||||
import { IExtensionDescription, IExtensionHostProfile, IExtensionService, ProfileSegmentId, ProfileSession } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
export class ExtensionHostProfiler {
|
||||
|
||||
constructor(private readonly _port: number, @IExtensionService private readonly _extensionService: IExtensionService) {
|
||||
}
|
||||
|
||||
public start(): TPromise<ProfileSession> {
|
||||
return TPromise.wrap(import('v8-inspect-profiler')).then(profiler => {
|
||||
return profiler.startProfiling({ port: this._port }).then(session => {
|
||||
return {
|
||||
stop: () => {
|
||||
return TPromise.wrap(session.stop()).then(profile => {
|
||||
return this._extensionService.getExtensions().then(extensions => {
|
||||
return this.distill(profile.profile, extensions);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
public async start(): Promise<ProfileSession> {
|
||||
const profiler = await import('v8-inspect-profiler');
|
||||
const session = await profiler.startProfiling({ port: this._port });
|
||||
return {
|
||||
stop: async () => {
|
||||
const profile = await session.stop();
|
||||
const extensions = await this._extensionService.getExtensions();
|
||||
return this.distill((profile as any).profile, extensions);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private distill(profile: Profile, extensions: IExtensionDescription[]): IExtensionHostProfile {
|
||||
|
||||
@@ -2,264 +2,63 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import pkg from 'vs/platform/node/package';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry';
|
||||
import { IMessage, IExtensionDescription, IExtensionsStatus, IExtensionService, ExtensionPointContribution, ActivationTimes, ProfileSession } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { USER_MANIFEST_CACHE_FILE, BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionEnablementService, IExtensionIdentifier, EnablementState, IExtensionManagementService, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions, BetterMergeId, BetterMergeDisabledNowKey, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionsRegistry, ExtensionPoint, IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { ExtensionScanner, ILog, ExtensionScannerInput, IExtensionResolver, IExtensionReference, Translations, IRelaxedExtensionDescription } from 'vs/workbench/services/extensions/node/extensionPoints';
|
||||
import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier';
|
||||
import { ExtHostContext, ExtHostExtensionServiceShape, IExtHostContext, MainContext } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { Barrier, runWhenIdle } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
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 { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { EnablementState, IExtensionEnablementService, IExtensionIdentifier, IExtensionManagementService, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { BetterMergeId, areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ExtensionHostProcessWorker } from 'vs/workbench/services/extensions/electron-browser/extensionHost';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ExtHostCustomersRegistry } from 'vs/workbench/api/electron-browser/extHostCustomers';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { mark } from 'vs/base/common/performance';
|
||||
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { Barrier } from 'vs/base/common/async';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler';
|
||||
import pkg from 'vs/platform/node/package';
|
||||
import product from 'vs/platform/node/product';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
let _SystemExtensionsRoot: string = null;
|
||||
function getSystemExtensionsRoot(): string {
|
||||
if (!_SystemExtensionsRoot) {
|
||||
_SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions'));
|
||||
}
|
||||
return _SystemExtensionsRoot;
|
||||
}
|
||||
let _ExtraDevSystemExtensionsRoot: string = null;
|
||||
function getExtraDevSystemExtensionsRoot(): string {
|
||||
if (!_ExtraDevSystemExtensionsRoot) {
|
||||
_ExtraDevSystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', '.build', 'builtInExtensions'));
|
||||
}
|
||||
return _ExtraDevSystemExtensionsRoot;
|
||||
}
|
||||
|
||||
interface IBuiltInExtension {
|
||||
name: string;
|
||||
version: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
interface IBuiltInExtensionControl {
|
||||
[name: string]: 'marketplace' | 'disabled' | string;
|
||||
}
|
||||
|
||||
class ExtraBuiltInExtensionResolver implements IExtensionResolver {
|
||||
|
||||
constructor(private builtInExtensions: IBuiltInExtension[], private control: IBuiltInExtensionControl) { }
|
||||
|
||||
resolveExtensions(): TPromise<IExtensionReference[]> {
|
||||
const result: IExtensionReference[] = [];
|
||||
|
||||
for (const ext of this.builtInExtensions) {
|
||||
const controlState = this.control[ext.name] || 'marketplace';
|
||||
|
||||
switch (controlState) {
|
||||
case 'disabled':
|
||||
break;
|
||||
case 'marketplace':
|
||||
result.push({ name: ext.name, path: path.join(getExtraDevSystemExtensionsRoot(), ext.name) });
|
||||
break;
|
||||
default:
|
||||
result.push({ name: ext.name, path: controlState });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return TPromise.as(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable to see detailed message communication between window and extension host
|
||||
const logExtensionHostCommunication = false;
|
||||
|
||||
function messageWithSource(source: string, message: string): string {
|
||||
if (source) {
|
||||
return `[${source}]: ${message}`;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
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 { 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';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
const NO_OP_VOID_PROMISE = TPromise.wrap<void>(void 0);
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(void 0);
|
||||
|
||||
export class ExtensionHostProcessManager extends Disposable {
|
||||
|
||||
public readonly onDidCrash: Event<[number, string]>;
|
||||
|
||||
/**
|
||||
* A map of already activated events to speed things up if the same activation event is triggered multiple times.
|
||||
*/
|
||||
private readonly _extensionHostProcessFinishedActivateEvents: { [activationEvent: string]: boolean; };
|
||||
private _extensionHostProcessRPCProtocol: RPCProtocol;
|
||||
private readonly _extensionHostProcessCustomers: IDisposable[];
|
||||
private readonly _extensionHostProcessWorker: ExtensionHostProcessWorker;
|
||||
/**
|
||||
* winjs believes a proxy is a promise because it has a `then` method, so wrap the result in an object.
|
||||
*/
|
||||
private _extensionHostProcessProxy: TPromise<{ value: ExtHostExtensionServiceShape; }>;
|
||||
|
||||
constructor(
|
||||
extensionHostProcessWorker: ExtensionHostProcessWorker,
|
||||
initialActivationEvents: string[],
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
) {
|
||||
super();
|
||||
this._extensionHostProcessFinishedActivateEvents = Object.create(null);
|
||||
this._extensionHostProcessRPCProtocol = null;
|
||||
this._extensionHostProcessCustomers = [];
|
||||
|
||||
this._extensionHostProcessWorker = extensionHostProcessWorker;
|
||||
this.onDidCrash = this._extensionHostProcessWorker.onCrashed;
|
||||
this._extensionHostProcessProxy = this._extensionHostProcessWorker.start().then(
|
||||
(protocol) => {
|
||||
return { value: this._createExtensionHostCustomers(protocol) };
|
||||
},
|
||||
(err) => {
|
||||
console.error('Error received from starting extension host');
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
this._extensionHostProcessProxy.then(() => {
|
||||
initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent));
|
||||
});
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
public getExtenstionHostProcessWorker(): ExtensionHostProcessWorker {
|
||||
return this._extensionHostProcessWorker;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._extensionHostProcessWorker) {
|
||||
this._extensionHostProcessWorker.dispose();
|
||||
}
|
||||
if (this._extensionHostProcessRPCProtocol) {
|
||||
this._extensionHostProcessRPCProtocol.dispose();
|
||||
}
|
||||
for (let i = 0, len = this._extensionHostProcessCustomers.length; i < len; i++) {
|
||||
const customer = this._extensionHostProcessCustomers[i];
|
||||
try {
|
||||
customer.dispose();
|
||||
} catch (err) {
|
||||
errors.onUnexpectedError(err);
|
||||
}
|
||||
}
|
||||
this._extensionHostProcessProxy = null;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public canProfileExtensionHost(): boolean {
|
||||
return this._extensionHostProcessWorker && Boolean(this._extensionHostProcessWorker.getInspectPort());
|
||||
}
|
||||
|
||||
private _createExtensionHostCustomers(protocol: IMessagePassingProtocol): ExtHostExtensionServiceShape {
|
||||
|
||||
if (logExtensionHostCommunication || this._environmentService.logExtensionHostCommunication) {
|
||||
protocol = asLoggingProtocol(protocol);
|
||||
}
|
||||
|
||||
this._extensionHostProcessRPCProtocol = new RPCProtocol(protocol);
|
||||
const extHostContext: IExtHostContext = {
|
||||
getProxy: <T>(identifier: ProxyIdentifier<T>): T => this._extensionHostProcessRPCProtocol.getProxy(identifier),
|
||||
set: <T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R => this._extensionHostProcessRPCProtocol.set(identifier, instance),
|
||||
assertRegistered: (identifiers: ProxyIdentifier<any>[]): void => this._extensionHostProcessRPCProtocol.assertRegistered(identifiers),
|
||||
};
|
||||
|
||||
// Named customers
|
||||
const namedCustomers = ExtHostCustomersRegistry.getNamedCustomers();
|
||||
for (let i = 0, len = namedCustomers.length; i < len; i++) {
|
||||
const [id, ctor] = namedCustomers[i];
|
||||
const instance = this._instantiationService.createInstance(ctor, extHostContext);
|
||||
this._extensionHostProcessCustomers.push(instance);
|
||||
this._extensionHostProcessRPCProtocol.set(id, instance);
|
||||
}
|
||||
|
||||
// Customers
|
||||
const customers = ExtHostCustomersRegistry.getCustomers();
|
||||
for (let i = 0, len = customers.length; i < len; i++) {
|
||||
const ctor = customers[i];
|
||||
const instance = this._instantiationService.createInstance(ctor, extHostContext);
|
||||
this._extensionHostProcessCustomers.push(instance);
|
||||
}
|
||||
|
||||
// Check that no named customers are missing
|
||||
const expected: ProxyIdentifier<any>[] = Object.keys(MainContext).map((key) => (<any>MainContext)[key]);
|
||||
this._extensionHostProcessRPCProtocol.assertRegistered(expected);
|
||||
|
||||
return this._extensionHostProcessRPCProtocol.getProxy(ExtHostContext.ExtHostExtensionService);
|
||||
}
|
||||
|
||||
public activateByEvent(activationEvent: string): TPromise<void> {
|
||||
if (this._extensionHostProcessFinishedActivateEvents[activationEvent] || !this._extensionHostProcessProxy) {
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
return this._extensionHostProcessProxy.then((proxy) => {
|
||||
if (!proxy) {
|
||||
// this case is already covered above and logged.
|
||||
// i.e. the extension host could not be started
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
return proxy.value.$activateByEvent(activationEvent);
|
||||
}).then(() => {
|
||||
this._extensionHostProcessFinishedActivateEvents[activationEvent] = true;
|
||||
});
|
||||
}
|
||||
|
||||
public startExtensionHostProfile(): TPromise<ProfileSession> {
|
||||
if (this._extensionHostProcessWorker) {
|
||||
let port = this._extensionHostProcessWorker.getInspectPort();
|
||||
if (port) {
|
||||
return this._instantiationService.createInstance(ExtensionHostProfiler, port).start();
|
||||
}
|
||||
}
|
||||
throw new Error('Extension host not running or no inspect port available');
|
||||
}
|
||||
}
|
||||
schema.properties.engines.properties.vscode.default = `^${pkg.version}`;
|
||||
|
||||
export class ExtensionService extends Disposable implements IExtensionService {
|
||||
|
||||
public _serviceBrand: any;
|
||||
|
||||
private readonly _onDidRegisterExtensions: Emitter<void>;
|
||||
|
||||
private readonly _extensionHostLogsLocation: URI;
|
||||
private _registry: ExtensionDescriptionRegistry;
|
||||
private readonly _installedExtensionsReady: Barrier;
|
||||
private readonly _isDev: boolean;
|
||||
private readonly _extensionsMessages: { [id: string]: IMessage[] };
|
||||
private _allRequestedActivateEvents: { [activationEvent: string]: boolean; };
|
||||
private readonly _extensionScanner: CachedExtensionScanner;
|
||||
|
||||
private readonly _onDidRegisterExtensions: Emitter<void> = this._register(new Emitter<void>({ leakWarningThreshold: 500 }));
|
||||
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 _onWillActivateByEvent = this._register(new Emitter<IWillActivateEvent>());
|
||||
public readonly onWillActivateByEvent: Event<IWillActivateEvent> = this._onWillActivateByEvent.event;
|
||||
|
||||
private readonly _onDidChangeResponsiveChange = this._register(new Emitter<IResponsiveStateChangeEvent>());
|
||||
public readonly onDidChangeResponsiveChange: Event<IResponsiveStateChangeEvent> = this._onDidChangeResponsiveChange.event;
|
||||
|
||||
// --- Members used per extension host process
|
||||
private _extensionHostProcessManagers: ExtensionHostProcessManager[];
|
||||
private _extensionHostProcessActivationTimes: { [id: string]: ActivationTimes; };
|
||||
@@ -271,25 +70,24 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@IExtensionEnablementService private readonly _extensionEnablementService: IExtensionEnablementService,
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IWindowService private readonly _windowService: IWindowService,
|
||||
@ILifecycleService lifecycleService: ILifecycleService,
|
||||
@IExtensionManagementService private extensionManagementService: IExtensionManagementService
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
|
||||
@IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService
|
||||
) {
|
||||
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._allRequestedActivateEvents = Object.create(null);
|
||||
|
||||
this._onDidRegisterExtensions = new Emitter<void>();
|
||||
this._extensionScanner = this._instantiationService.createInstance(CachedExtensionScanner);
|
||||
|
||||
this._extensionHostProcessManagers = [];
|
||||
this._extensionHostProcessActivationTimes = Object.create(null);
|
||||
this._extensionHostExtensionRuntimeErrors = Object.create(null);
|
||||
|
||||
this.startDelayed(lifecycleService);
|
||||
this._startDelayed(this._lifecycleService);
|
||||
|
||||
if (this._extensionEnablementService.allUserExtensionsDisabled) {
|
||||
this._notificationService.prompt(Severity.Info, nls.localize('extensionsDisabled', "All installed extensions are temporarily disabled. Reload the window to return to the previous state."), [{
|
||||
@@ -301,58 +99,37 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
public getExtenstionHostProcessId(): number {
|
||||
if (this._extensionHostProcessManagers.length !== 1)
|
||||
{
|
||||
this._logOrShowMessage(Severity.Warning, 'Exactly one Extension Host Process Manager was expected');
|
||||
}
|
||||
return this._extensionHostProcessManagers[0].getExtenstionHostProcessWorker().getExtenstionHostProcess().pid;
|
||||
}
|
||||
private startDelayed(lifecycleService: ILifecycleService): void {
|
||||
let started = false;
|
||||
const startOnce = () => {
|
||||
if (!started) {
|
||||
started = true;
|
||||
|
||||
this._startExtensionHostProcess([]);
|
||||
this._scanAndHandleExtensions();
|
||||
}
|
||||
};
|
||||
|
||||
private _startDelayed(lifecycleService: ILifecycleService): void {
|
||||
// delay extension host creation and extension scanning
|
||||
// until the workbench is restoring. we cannot defer the
|
||||
// extension host more (LifecyclePhase.Running) because
|
||||
// until the workbench is running. we cannot defer the
|
||||
// extension host more (LifecyclePhase.Restored) because
|
||||
// some editors require the extension host to restore
|
||||
// and this would result in a deadlock
|
||||
// see https://github.com/Microsoft/vscode/issues/41322
|
||||
lifecycleService.when(LifecyclePhase.Restoring).then(() => {
|
||||
// we add an additional delay of 800ms because the extension host
|
||||
// starting is a potential expensive operation and we do no want
|
||||
// to fight with editors, viewlets and panels restoring.
|
||||
setTimeout(() => startOnce(), 800);
|
||||
lifecycleService.when(LifecyclePhase.Ready).then(() => {
|
||||
// reschedule to ensure this runs after restoring viewlets, panels, and editors
|
||||
runWhenIdle(() => {
|
||||
perf.mark('willLoadExtensions');
|
||||
this._startExtensionHostProcess(true, []);
|
||||
this._scanAndHandleExtensions();
|
||||
this.whenInstalledExtensionsRegistered().then(() => perf.mark('didLoadExtensions'));
|
||||
}, 50 /*max delay*/);
|
||||
});
|
||||
|
||||
// if we are running before the 800ms delay, make sure to start
|
||||
// the extension host right away though.
|
||||
lifecycleService.when(LifecyclePhase.Running).then(() => startOnce());
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public get onDidRegisterExtensions(): Event<void> {
|
||||
return this._onDidRegisterExtensions.event;
|
||||
this._onWillActivateByEvent.dispose();
|
||||
this._onDidChangeResponsiveChange.dispose();
|
||||
}
|
||||
|
||||
public restartExtensionHost(): void {
|
||||
this._stopExtensionHostProcess();
|
||||
this._startExtensionHostProcess(Object.keys(this._allRequestedActivateEvents));
|
||||
this._startExtensionHostProcess(false, Object.keys(this._allRequestedActivateEvents));
|
||||
}
|
||||
|
||||
public startExtensionHost(): void {
|
||||
this._startExtensionHostProcess(Object.keys(this._allRequestedActivateEvents));
|
||||
this._startExtensionHostProcess(false, Object.keys(this._allRequestedActivateEvents));
|
||||
}
|
||||
|
||||
public stopExtensionHost(): void {
|
||||
@@ -374,12 +151,13 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
}
|
||||
|
||||
private _startExtensionHostProcess(initialActivationEvents: string[]): void {
|
||||
private _startExtensionHostProcess(isInitialStart: boolean, initialActivationEvents: string[]): void {
|
||||
this._stopExtensionHostProcess();
|
||||
|
||||
const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, this.getExtensions());
|
||||
const extHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, extHostProcessWorker, initialActivationEvents);
|
||||
const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, !isInitialStart, this.getExtensions(), 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 }); });
|
||||
this._extensionHostProcessManagers.push(extHostProcessManager);
|
||||
}
|
||||
|
||||
@@ -387,6 +165,23 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
console.error('Extension host terminated unexpectedly. Code: ', code, ' Signal: ', signal);
|
||||
this._stopExtensionHostProcess();
|
||||
|
||||
if (code === 55) {
|
||||
this._notificationService.prompt(
|
||||
Severity.Error,
|
||||
nls.localize('extensionHostProcess.versionMismatchCrash', "Extension host cannot start: version mismatch."),
|
||||
[{
|
||||
label: nls.localize('relaunch', "Relaunch VS Code"),
|
||||
run: () => {
|
||||
this._instantiationService.invokeFunction((accessor) => {
|
||||
const windowsService = accessor.get(IWindowsService);
|
||||
windowsService.relaunch({});
|
||||
});
|
||||
}
|
||||
}]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let message = nls.localize('extensionHostProcess.crash', "Extension host terminated unexpectedly.");
|
||||
if (code === 87) {
|
||||
message = nls.localize('extensionHostProcess.unresponsiveCrash', "Extension host terminated because it was not responsive.");
|
||||
@@ -399,14 +194,14 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
},
|
||||
{
|
||||
label: nls.localize('restart', "Restart Extension Host"),
|
||||
run: () => this._startExtensionHostProcess(Object.keys(this._allRequestedActivateEvents))
|
||||
run: () => this._startExtensionHostProcess(false, Object.keys(this._allRequestedActivateEvents))
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
// ---- begin IExtensionService
|
||||
|
||||
public activateByEvent(activationEvent: string): TPromise<void> {
|
||||
public activateByEvent(activationEvent: string): Promise<void> {
|
||||
if (this._installedExtensionsReady.isOpen()) {
|
||||
// Extensions have been scanned and interpreted
|
||||
|
||||
@@ -429,23 +224,34 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
}
|
||||
|
||||
private _activateByEvent(activationEvent: string): TPromise<void> {
|
||||
return TPromise.join(
|
||||
private _activateByEvent(activationEvent: string): Promise<void> {
|
||||
const result = Promise.all(
|
||||
this._extensionHostProcessManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent))
|
||||
).then(() => { });
|
||||
this._onWillActivateByEvent.fire({
|
||||
event: activationEvent,
|
||||
activation: result
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public whenInstalledExtensionsRegistered(): TPromise<boolean> {
|
||||
public whenInstalledExtensionsRegistered(): Promise<boolean> {
|
||||
return this._installedExtensionsReady.wait();
|
||||
}
|
||||
|
||||
public getExtensions(): TPromise<IExtensionDescription[]> {
|
||||
public getExtensions(): Promise<IExtensionDescription[]> {
|
||||
return this._installedExtensionsReady.wait().then(() => {
|
||||
return this._registry.getAllExtensionDescriptions();
|
||||
});
|
||||
}
|
||||
|
||||
public readExtensionPointContributions<T>(extPoint: IExtensionPoint<T>): TPromise<ExtensionPointContribution<T>[]> {
|
||||
public getExtension(id: string): Promise<IExtensionDescription | undefined> {
|
||||
return this._installedExtensionsReady.wait().then(() => {
|
||||
return this._registry.getExtensionDescription(id);
|
||||
});
|
||||
}
|
||||
|
||||
public readExtensionPointContributions<T>(extPoint: IExtensionPoint<T>): Promise<ExtensionPointContribution<T>[]> {
|
||||
return this._installedExtensionsReady.wait().then(() => {
|
||||
let availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
|
||||
@@ -489,7 +295,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
return false;
|
||||
}
|
||||
|
||||
public startExtensionHostProfile(): TPromise<ProfileSession> {
|
||||
public startExtensionHostProfile(): Promise<ProfileSession> {
|
||||
for (let i = 0, len = this._extensionHostProcessManagers.length; i < len; i++) {
|
||||
const extHostProcessManager = this._extensionHostProcessManagers[i];
|
||||
if (extHostProcessManager.canProfileExtensionHost()) {
|
||||
@@ -499,59 +305,49 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
throw new Error('Extension host not running or no inspect port available');
|
||||
}
|
||||
|
||||
public getInspectPort(): number {
|
||||
if (this._extensionHostProcessManagers.length > 0) {
|
||||
return this._extensionHostProcessManagers[0].getInspectPort();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- end IExtensionService
|
||||
|
||||
// --- impl
|
||||
|
||||
private _scanAndHandleExtensions(): void {
|
||||
private async _scanAndHandleExtensions(): Promise<void> {
|
||||
this._extensionScanner.startScanningExtensions(new Logger((severity, source, message) => {
|
||||
if (this._isDev && source) {
|
||||
this._logOrShowMessage(severity, `[${source}]: ${message}`);
|
||||
} else {
|
||||
this._logOrShowMessage(severity, message);
|
||||
}
|
||||
}));
|
||||
|
||||
this._scanExtensions()
|
||||
.then(allExtensions => this._getRuntimeExtensions(allExtensions))
|
||||
.then(allExtensions => {
|
||||
this._registry = new ExtensionDescriptionRegistry(allExtensions);
|
||||
|
||||
let availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
let extensionPoints = ExtensionsRegistry.getExtensionPoints();
|
||||
|
||||
let messageHandler = (msg: IMessage) => this._handleExtensionPointMessage(msg);
|
||||
|
||||
for (let i = 0, len = extensionPoints.length; i < len; i++) {
|
||||
ExtensionService._handleExtensionPoint(extensionPoints[i], availableExtensions, messageHandler);
|
||||
}
|
||||
|
||||
mark('extensionHostReady');
|
||||
this._installedExtensionsReady.open();
|
||||
this._onDidRegisterExtensions.fire(void 0);
|
||||
this._onDidChangeExtensionsStatus.fire(availableExtensions.map(e => e.id));
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
private _scanExtensions(): TPromise<IExtensionDescription[]> {
|
||||
const log = new Logger((severity, source, message) => {
|
||||
this._logOrShowMessage(severity, this._isDev ? messageWithSource(source, message) : message);
|
||||
});
|
||||
private _onHasExtensions(allExtensions: IExtensionDescription[]): void {
|
||||
this._registry = new ExtensionDescriptionRegistry(allExtensions);
|
||||
|
||||
return ExtensionService._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]);
|
||||
});
|
||||
let availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
let extensionPoints = ExtensionsRegistry.getExtensionPoints();
|
||||
|
||||
let messageHandler = (msg: IMessage) => this._handleExtensionPointMessage(msg);
|
||||
|
||||
for (let i = 0, len = extensionPoints.length; i < len; i++) {
|
||||
ExtensionService._handleExtensionPoint(extensionPoints[i], availableExtensions, messageHandler);
|
||||
}
|
||||
|
||||
perf.mark('extensionHostReady');
|
||||
this._installedExtensionsReady.open();
|
||||
this._onDidRegisterExtensions.fire(void 0);
|
||||
this._onDidChangeExtensionsStatus.fire(availableExtensions.map(e => e.id));
|
||||
}
|
||||
|
||||
private _getRuntimeExtensions(allExtensions: IExtensionDescription[]): Promise<IExtensionDescription[]> {
|
||||
@@ -564,12 +360,23 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
|
||||
const 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)) {
|
||||
console.error(notFound(id));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const enableProposedApiForAll = !this._environmentService.isBuilt ||
|
||||
(!!this._environmentService.extensionDevelopmentPath && product.nameLong.indexOf('Insiders') >= 0) ||
|
||||
(!!this._environmentService.extensionDevelopmentLocationURI && product.nameLong.indexOf('Insiders') >= 0) ||
|
||||
(enableProposedApiFor.length === 0 && 'enable-proposed-api' in this._environmentService.args);
|
||||
|
||||
for (const extension of allExtensions) {
|
||||
const isExtensionUnderDevelopment = this._environmentService.isExtensionDevelopment && extension.extensionLocation.scheme === Schemas.file && extension.extensionLocation.fsPath.indexOf(this._environmentService.extensionDevelopmentPath) === 0;
|
||||
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))) {
|
||||
@@ -595,13 +402,12 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
});
|
||||
|
||||
if (extensionsToDisable.length) {
|
||||
return this.extensionManagementService.getInstalled(LocalExtensionType.User)
|
||||
return this._extensionManagementService.getInstalled(LocalExtensionType.User)
|
||||
.then(installed => {
|
||||
const toDisable = installed.filter(i => extensionsToDisable.some(e => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i) }, e)));
|
||||
return TPromise.join(toDisable.map(e => this._extensionEnablementService.setEnablement(e, EnablementState.Disabled)));
|
||||
return Promise.all(toDisable.map(e => this._extensionEnablementService.setEnablement(e, EnablementState.Disabled)));
|
||||
})
|
||||
.then(() => {
|
||||
this._storageService.store(BetterMergeDisabledNowKey, true);
|
||||
return runtimeExtensions;
|
||||
});
|
||||
} else {
|
||||
@@ -611,7 +417,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
|
||||
private _updateEnableProposedApi(extension: IExtensionDescription, enableProposedApiForAll: boolean, enableProposedApiFor: string | string[]): IExtensionDescription {
|
||||
if (!isFalsyOrEmpty(product.extensionAllowedProposedApi)
|
||||
if (isNonEmptyArray(product.extensionAllowedProposedApi)
|
||||
&& product.extensionAllowedProposedApi.indexOf(extension.id) >= 0
|
||||
) {
|
||||
// fast lane -> proposed api is available to all extensions
|
||||
@@ -667,222 +473,6 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
}
|
||||
|
||||
private static async _validateExtensionsCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): Promise<void> {
|
||||
const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER);
|
||||
const cacheFile = path.join(cacheFolder, cacheKey);
|
||||
|
||||
const expected = JSON.parse(JSON.stringify(await ExtensionScanner.scanExtensions(input, new NullLogger())));
|
||||
|
||||
const cacheContents = await this._readExtensionCache(environmentService, cacheKey);
|
||||
if (!cacheContents) {
|
||||
// Cache has been deleted by someone else, which is perfectly fine...
|
||||
return;
|
||||
}
|
||||
const actual = cacheContents.result;
|
||||
|
||||
if (objects.equals(expected, actual)) {
|
||||
// Cache is valid and running with it is perfectly fine...
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await pfs.del(cacheFile);
|
||||
} catch (err) {
|
||||
errors.onUnexpectedError(err);
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
notificationService.prompt(
|
||||
Severity.Error,
|
||||
nls.localize('extensionCache.invalid', "Extensions have been modified on disk. Please reload the window."),
|
||||
[{
|
||||
label: nls.localize('reloadWindow', "Reload Window"),
|
||||
run: () => windowService.reloadWindow()
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
private static async _readExtensionCache(environmentService: IEnvironmentService, cacheKey: string): Promise<IExtensionCacheData> {
|
||||
const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER);
|
||||
const cacheFile = path.join(cacheFolder, cacheKey);
|
||||
|
||||
try {
|
||||
const cacheRawContents = await pfs.readFile(cacheFile, 'utf8');
|
||||
return JSON.parse(cacheRawContents);
|
||||
} catch (err) {
|
||||
// That's ok...
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async _writeExtensionCache(environmentService: IEnvironmentService, cacheKey: string, cacheContents: IExtensionCacheData): Promise<void> {
|
||||
const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER);
|
||||
const cacheFile = path.join(cacheFolder, cacheKey);
|
||||
|
||||
try {
|
||||
await pfs.mkdirp(cacheFolder);
|
||||
} catch (err) {
|
||||
// That's ok...
|
||||
}
|
||||
|
||||
try {
|
||||
await pfs.writeFile(cacheFile, JSON.stringify(cacheContents));
|
||||
} catch (err) {
|
||||
// That's ok...
|
||||
}
|
||||
}
|
||||
|
||||
private static async _scanExtensionsWithCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput, log: ILog): Promise<IExtensionDescription[]> {
|
||||
if (input.devMode) {
|
||||
// Do not cache when running out of sources...
|
||||
return ExtensionScanner.scanExtensions(input, log);
|
||||
}
|
||||
|
||||
try {
|
||||
const folderStat = await pfs.stat(input.absoluteFolderPath);
|
||||
input.mtime = folderStat.mtime.getTime();
|
||||
} catch (err) {
|
||||
// That's ok...
|
||||
}
|
||||
|
||||
const cacheContents = await this._readExtensionCache(environmentService, cacheKey);
|
||||
if (cacheContents && cacheContents.input && ExtensionScannerInput.equals(cacheContents.input, input)) {
|
||||
// Validate the cache asynchronously after 5s
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this._validateExtensionsCache(windowService, notificationService, environmentService, cacheKey, input);
|
||||
} catch (err) {
|
||||
errors.onUnexpectedError(err);
|
||||
}
|
||||
}, 5000);
|
||||
return cacheContents.result.map((extensionDescription) => {
|
||||
// revive URI object
|
||||
(<IRelaxedExtensionDescription>extensionDescription).extensionLocation = URI.revive(extensionDescription.extensionLocation);
|
||||
return extensionDescription;
|
||||
});
|
||||
}
|
||||
|
||||
const counterLogger = new CounterLogger(log);
|
||||
const result = await ExtensionScanner.scanExtensions(input, counterLogger);
|
||||
if (counterLogger.errorCnt === 0) {
|
||||
// Nothing bad happened => cache the result
|
||||
const cacheContents: IExtensionCacheData = {
|
||||
input: input,
|
||||
result: result
|
||||
};
|
||||
await this._writeExtensionCache(environmentService, cacheKey, cacheContents);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _scanInstalledExtensions(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, extensionEnablementService: IExtensionEnablementService, log: ILog): TPromise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> {
|
||||
|
||||
const translationConfig: TPromise<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);
|
||||
})
|
||||
: TPromise.as(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: TPromise<IExtensionDescription[]> = TPromise.wrap(builtinExtensions);
|
||||
|
||||
if (devMode) {
|
||||
const builtInExtensionsFilePath = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', '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 = TPromise.join([builtInExtensions, controlFile])
|
||||
.then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control))
|
||||
.then(resolver => ExtensionScanner.scanExtensions(input, log, resolver));
|
||||
|
||||
finalBuiltinExtensions = TPromise.join([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];
|
||||
}
|
||||
// Overwrite with extensions found in extra
|
||||
for (let i = 0, len = extraBuiltinExtensions.length; i < len; i++) {
|
||||
resultMap[extraBuiltinExtensions[i].id] = extraBuiltinExtensions[i];
|
||||
}
|
||||
|
||||
let resultArr = Object.keys(resultMap).map((id) => resultMap[id]);
|
||||
resultArr.sort((a, b) => {
|
||||
const aLastSegment = path.basename(a.extensionLocation.fsPath);
|
||||
const bLastSegment = path.basename(b.extensionLocation.fsPath);
|
||||
if (aLastSegment < bLastSegment) {
|
||||
return -1;
|
||||
}
|
||||
if (aLastSegment > bLastSegment) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return resultArr;
|
||||
});
|
||||
}
|
||||
|
||||
const userExtensions = (
|
||||
extensionEnablementService.allUserExtensionsDisabled || !environmentService.extensionsPath
|
||||
? TPromise.as([])
|
||||
: 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
|
||||
const developedExtensions = (
|
||||
environmentService.isExtensionDevelopment
|
||||
? ExtensionScanner.scanOneOrMultipleExtensions(
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionDevelopmentPath, false, true, translations), log
|
||||
)
|
||||
: TPromise.as([])
|
||||
);
|
||||
|
||||
return TPromise.join([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: [] };
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private static _handleExtensionPoint<T>(extensionPoint: ExtensionPoint<T>, availableExtensions: IExtensionDescription[], messageHandler: (msg: IMessage) => void): void {
|
||||
let users: IExtensionPointUser<T>[] = [], usersLen = 0;
|
||||
for (let i = 0, len = availableExtensions.length; i < len; i++) {
|
||||
@@ -954,78 +544,3 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
this._onDidChangeExtensionsStatus.fire([extensionId]);
|
||||
}
|
||||
}
|
||||
|
||||
function asLoggingProtocol(protocol: IMessagePassingProtocol): IMessagePassingProtocol {
|
||||
|
||||
protocol.onMessage(msg => {
|
||||
console.log('%c[Extension \u2192 Window]%c[len: ' + strings.pad(msg.length, 5, ' ') + ']', 'color: darkgreen', 'color: grey', msg);
|
||||
});
|
||||
|
||||
return {
|
||||
onMessage: protocol.onMessage,
|
||||
|
||||
send(msg: any) {
|
||||
protocol.send(msg);
|
||||
console.log('%c[Window \u2192 Extension]%c[len: ' + strings.pad(msg.length, 5, ' ') + ']', 'color: darkgreen', 'color: grey', msg);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface IExtensionCacheData {
|
||||
input: ExtensionScannerInput;
|
||||
result: IExtensionDescription[];
|
||||
}
|
||||
|
||||
export class Logger implements ILog {
|
||||
|
||||
private readonly _messageHandler: (severity: Severity, source: string, message: string) => void;
|
||||
|
||||
constructor(
|
||||
messageHandler: (severity: Severity, source: string, message: string) => void
|
||||
) {
|
||||
this._messageHandler = messageHandler;
|
||||
}
|
||||
|
||||
public error(source: string, message: string): void {
|
||||
this._messageHandler(Severity.Error, source, message);
|
||||
}
|
||||
|
||||
public warn(source: string, message: string): void {
|
||||
this._messageHandler(Severity.Warning, source, message);
|
||||
}
|
||||
|
||||
public info(source: string, message: string): void {
|
||||
this._messageHandler(Severity.Info, source, message);
|
||||
}
|
||||
}
|
||||
|
||||
class CounterLogger implements ILog {
|
||||
|
||||
public errorCnt = 0;
|
||||
public warnCnt = 0;
|
||||
public infoCnt = 0;
|
||||
|
||||
constructor(private readonly _actual: ILog) {
|
||||
}
|
||||
|
||||
public error(source: string, message: string): void {
|
||||
this._actual.error(source, message);
|
||||
}
|
||||
|
||||
public warn(source: string, message: string): void {
|
||||
this._actual.warn(source, message);
|
||||
}
|
||||
|
||||
public info(source: string, message: string): void {
|
||||
this._actual.info(source, message);
|
||||
}
|
||||
}
|
||||
|
||||
class NullLogger implements ILog {
|
||||
public error(source: string, message: string): void {
|
||||
}
|
||||
public warn(source: string, message: string): void {
|
||||
}
|
||||
public info(source: string, message: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationHandle, INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
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';
|
||||
|
||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||
const THIRTY_SECONDS = 30 * 1000;
|
||||
const URL_TO_HANDLE = 'extensionUrlHandler.urlToHandle';
|
||||
|
||||
function isExtensionId(value: string): boolean {
|
||||
return /^[a-z0-9][a-z0-9\-]*\.[a-z0-9][a-z0-9\-]*$/i.test(value);
|
||||
}
|
||||
|
||||
export const IExtensionUrlHandler = createDecorator<IExtensionUrlHandler>('inactiveExtensionUrlHandler');
|
||||
|
||||
export interface IExtensionUrlHandler {
|
||||
readonly _serviceBrand: any;
|
||||
registerExtensionHandler(extensionId: string, handler: IURLHandler): void;
|
||||
unregisterExtensionHandler(extensionId: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class handles URLs which are directed towards inactive extensions.
|
||||
* If a URL is directed towards an inactive extension, it buffers it,
|
||||
* activates the extension and re-opens the URL once the extension registers
|
||||
* a URL handler. If the extension never registers a URL handler, the urls
|
||||
* will eventually be garbage collected.
|
||||
*
|
||||
* It also makes sure the user confirms opening URLs directed towards extensions.
|
||||
*/
|
||||
export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
|
||||
|
||||
readonly _serviceBrand: any;
|
||||
|
||||
private extensionHandlers = new Map<string, IURLHandler>();
|
||||
private uriBuffer = new Map<string, { timestamp: number, uri: URI }[]>();
|
||||
private disposable: IDisposable;
|
||||
|
||||
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
|
||||
) {
|
||||
const interval = setInterval(() => this.garbageCollect(), THIRTY_SECONDS);
|
||||
const urlToHandleValue = this.storageService.get(URL_TO_HANDLE, StorageScope.WORKSPACE);
|
||||
if (urlToHandleValue) {
|
||||
this.storageService.remove(URL_TO_HANDLE, StorageScope.WORKSPACE);
|
||||
this.handleURL(URI.revive(JSON.parse(urlToHandleValue)), true);
|
||||
}
|
||||
|
||||
this.disposable = combinedDisposable([
|
||||
urlService.registerHandler(this),
|
||||
toDisposable(() => clearInterval(interval))
|
||||
]);
|
||||
}
|
||||
|
||||
handleURL(uri: URI, confirmed?: boolean): TPromise<boolean> {
|
||||
if (!isExtensionId(uri.authority)) {
|
||||
return TPromise.as(false);
|
||||
}
|
||||
|
||||
const extensionId = uri.authority;
|
||||
const wasHandlerAvailable = this.extensionHandlers.has(extensionId);
|
||||
|
||||
return this.extensionService.getExtension(extensionId).then(extension => {
|
||||
|
||||
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({
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
registerExtensionHandler(extensionId: string, handler: IURLHandler): void {
|
||||
this.extensionHandlers.set(extensionId, handler);
|
||||
|
||||
const uris = this.uriBuffer.get(extensionId) || [];
|
||||
|
||||
for (const { uri } of uris) {
|
||||
handler.handleURL(uri);
|
||||
}
|
||||
|
||||
this.uriBuffer.delete(extensionId);
|
||||
}
|
||||
|
||||
unregisterExtensionHandler(extensionId: string): void {
|
||||
this.extensionHandlers.delete(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];
|
||||
|
||||
// Extension is installed
|
||||
if (extension) {
|
||||
const enabled = this.extensionEnablementService.isEnabled(extension);
|
||||
|
||||
// Extension is not running. Reload the window to handle.
|
||||
if (enabled) {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// Extension is disabled. Enable the extension and reload the window to handle.
|
||||
else {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private reloadAndHandle(url: URI): TPromise<void> {
|
||||
this.storageService.store(URL_TO_HANDLE, JSON.stringify(url.toJSON()), StorageScope.WORKSPACE);
|
||||
return this.windowService.reloadWindow();
|
||||
}
|
||||
|
||||
// forget about all uris buffered more than 5 minutes ago
|
||||
private garbageCollect(): void {
|
||||
const now = new Date().getTime();
|
||||
const uriBuffer = new Map<string, { timestamp: number, uri: URI }[]>();
|
||||
|
||||
this.uriBuffer.forEach((uris, extensionId) => {
|
||||
uris = uris.filter(({ timestamp }) => now - timestamp < FIVE_MINUTES);
|
||||
|
||||
if (uris.length > 0) {
|
||||
uriBuffer.set(extensionId, uris);
|
||||
}
|
||||
});
|
||||
|
||||
this.uriBuffer = uriBuffer;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposable.dispose();
|
||||
this.extensionHandlers.clear();
|
||||
this.uriBuffer.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { EditorInput } from 'vs/workbench/common/editor';
|
||||
|
||||
export class RuntimeExtensionsInput extends EditorInput {
|
||||
|
||||
static readonly ID = 'workbench.runtimeExtensions.input';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
getTypeId(): string {
|
||||
return RuntimeExtensionsInput.ID;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return nls.localize('extensionsInputName', "Running Extensions");
|
||||
}
|
||||
|
||||
matches(other: any): boolean {
|
||||
if (!(other instanceof RuntimeExtensionsInput)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
resolve(): Thenable<any> {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
supportsSplitEditor(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getResource(): URI {
|
||||
return URI.from({
|
||||
scheme: 'runtime-extensions',
|
||||
path: 'default'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,29 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
|
||||
export class ExtensionDescriptionRegistry {
|
||||
private _extensionDescriptions: IExtensionDescription[];
|
||||
private _extensionsMap: { [extensionId: string]: IExtensionDescription; };
|
||||
private _extensionsArr: IExtensionDescription[];
|
||||
private _activationMap: { [activationEvent: string]: IExtensionDescription[]; };
|
||||
|
||||
constructor(extensionDescriptions: IExtensionDescription[]) {
|
||||
this._extensionDescriptions = extensionDescriptions;
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._extensionsMap = {};
|
||||
this._extensionsArr = [];
|
||||
this._activationMap = {};
|
||||
|
||||
for (let i = 0, len = extensionDescriptions.length; i < len; i++) {
|
||||
let extensionDescription = extensionDescriptions[i];
|
||||
for (let i = 0, len = this._extensionDescriptions.length; i < len; i++) {
|
||||
let extensionDescription = this._extensionDescriptions[i];
|
||||
|
||||
if (hasOwnProperty.call(this._extensionsMap, extensionDescription.id)) {
|
||||
// No overwriting allowed!
|
||||
@@ -46,6 +51,13 @@ export class ExtensionDescriptionRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
public containsActivationEvent(activationEvent: string): boolean {
|
||||
return hasOwnProperty.call(this._activationMap, activationEvent);
|
||||
}
|
||||
@@ -61,7 +73,7 @@ export class ExtensionDescriptionRegistry {
|
||||
return this._extensionsArr.slice(0);
|
||||
}
|
||||
|
||||
public getExtensionDescription(extensionId: string): IExtensionDescription {
|
||||
public getExtensionDescription(extensionId: string): IExtensionDescription | null {
|
||||
if (!hasOwnProperty.call(this._extensionsMap, extensionId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,28 +3,44 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionManagementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtensionManagementServer, IExtensionManagementServerService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/node/extensionManagementIpc';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/node/remoteAgentService';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { IChannel } from 'vs/base/parts/ipc/node/ipc';
|
||||
|
||||
const localExtensionManagementServerAuthority: string = 'vscode-local';
|
||||
|
||||
export class ExtensionManagementServerService implements IExtensionManagementServerService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly extensionManagementServers: IExtensionManagementServer[];
|
||||
readonly localExtensionManagementServer: IExtensionManagementServer;
|
||||
readonly remoteExtensionManagementServer: IExtensionManagementServer | null = null;
|
||||
|
||||
constructor(
|
||||
localExtensionManagementService: IExtensionManagementService
|
||||
localExtensionManagementService: IExtensionManagementService,
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService
|
||||
) {
|
||||
this.extensionManagementServers = [{ extensionManagementService: localExtensionManagementService, location: URI.from({ scheme: Schemas.file }) }];
|
||||
this.localExtensionManagementServer = { extensionManagementService: localExtensionManagementService, authority: localExtensionManagementServerAuthority, label: localize('local', "Local") };
|
||||
const remoteAgentConnection = remoteAgentService.getConnection();
|
||||
if (remoteAgentConnection) {
|
||||
const extensionManagementService = new ExtensionManagementChannelClient(remoteAgentConnection.getChannel<IChannel>('extensions'));
|
||||
this.remoteExtensionManagementServer = { authority: remoteAgentConnection.remoteAuthority, extensionManagementService, label: remoteAgentConnection.remoteAuthority };
|
||||
}
|
||||
}
|
||||
|
||||
getExtensionManagementServer(location: URI): IExtensionManagementServer {
|
||||
return this.extensionManagementServers[0];
|
||||
}
|
||||
|
||||
getDefaultExtensionManagementServer(): IExtensionManagementServer {
|
||||
return this.extensionManagementServers[0];
|
||||
getExtensionManagementServer(location: URI): IExtensionManagementServer | null {
|
||||
if (location.scheme === Schemas.file) {
|
||||
return this.localExtensionManagementServer;
|
||||
}
|
||||
if (location.scheme === REMOTE_HOST_SCHEME) {
|
||||
return this.remoteExtensionManagementServer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,20 +48,22 @@ export class SingleServerExtensionManagementServerService implements IExtensionM
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly extensionManagementServers: IExtensionManagementServer[];
|
||||
|
||||
constructor(
|
||||
extensionManagementServer: IExtensionManagementServer
|
||||
private readonly extensionManagementServer: IExtensionManagementServer
|
||||
) {
|
||||
this.extensionManagementServers = [extensionManagementServer];
|
||||
}
|
||||
|
||||
getExtensionManagementServer(location: URI): IExtensionManagementServer {
|
||||
location = location.scheme === Schemas.file ? URI.from({ scheme: Schemas.file }) : location;
|
||||
return this.extensionManagementServers.filter(server => location.authority === server.location.authority)[0];
|
||||
getExtensionManagementServer(location: URI): IExtensionManagementServer | null {
|
||||
const authority = location.scheme === Schemas.file ? localExtensionManagementServerAuthority : location.authority;
|
||||
return this.extensionManagementServer.authority === authority ? this.extensionManagementServer : null;
|
||||
}
|
||||
|
||||
getDefaultExtensionManagementServer(): IExtensionManagementServer {
|
||||
return this.extensionManagementServers[0];
|
||||
get localExtensionManagementServer(): IExtensionManagementServer | null {
|
||||
return this.extensionManagementServer.authority === localExtensionManagementServerAuthority ? this.extensionManagementServer : null;
|
||||
}
|
||||
|
||||
get remoteExtensionManagementServer(): IExtensionManagementServer | null {
|
||||
return this.extensionManagementServer.authority !== localExtensionManagementServerAuthority ? this.extensionManagementServer : null;
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,19 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { join, normalize, extname } from 'path';
|
||||
import * as json from 'vs/base/common/json';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { isValidExtensionVersion } from 'vs/platform/extensions/node/extensionValidator';
|
||||
import * as path from 'path';
|
||||
import * as semver from 'semver';
|
||||
import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
|
||||
import * as json from 'vs/base/common/json';
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
|
||||
import { groupByExtension, getGalleryExtensionId, getLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import URI from 'vs/base/common/uri';
|
||||
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 { isValidExtensionVersion } from 'vs/platform/extensions/node/extensionValidator';
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
const MANIFEST_FILE = 'package.json';
|
||||
|
||||
@@ -51,7 +49,7 @@ namespace Translations {
|
||||
|
||||
export interface NlsConfiguration {
|
||||
readonly devMode: boolean;
|
||||
readonly locale: string;
|
||||
readonly locale: string | undefined;
|
||||
readonly pseudo: boolean;
|
||||
readonly translations: Translations;
|
||||
}
|
||||
@@ -77,13 +75,13 @@ abstract class ExtensionManifestHandler {
|
||||
this._absoluteFolderPath = absoluteFolderPath;
|
||||
this._isBuiltin = isBuiltin;
|
||||
this._isUnderDevelopment = isUnderDevelopment;
|
||||
this._absoluteManifestPath = join(absoluteFolderPath, MANIFEST_FILE);
|
||||
this._absoluteManifestPath = path.join(absoluteFolderPath, MANIFEST_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionManifestParser extends ExtensionManifestHandler {
|
||||
|
||||
public parse(): TPromise<IExtensionDescription> {
|
||||
public parse(): Promise<IExtensionDescription> {
|
||||
return pfs.readFile(this._absoluteManifestPath).then((manifestContents) => {
|
||||
try {
|
||||
const manifest = JSON.parse(manifestContents.toString());
|
||||
@@ -120,7 +118,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
|
||||
this._nlsConfig = nlsConfig;
|
||||
}
|
||||
|
||||
public replaceNLS(extensionDescription: IExtensionDescription): TPromise<IExtensionDescription> {
|
||||
public replaceNLS(extensionDescription: IExtensionDescription): Promise<IExtensionDescription> {
|
||||
interface MessageBag {
|
||||
[key: string]: string;
|
||||
}
|
||||
@@ -132,22 +130,22 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
|
||||
}
|
||||
|
||||
interface LocalizedMessages {
|
||||
values: MessageBag;
|
||||
default: string;
|
||||
values: MessageBag | undefined;
|
||||
default: string | null;
|
||||
}
|
||||
|
||||
const reportErrors = (localized: string, errors: json.ParseError[]): void => {
|
||||
const reportErrors = (localized: string | null, errors: json.ParseError[]): void => {
|
||||
errors.forEach((error) => {
|
||||
this._log.error(this._absoluteFolderPath, nls.localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localized, getParseErrorMessage(error.error)));
|
||||
});
|
||||
};
|
||||
|
||||
let extension = extname(this._absoluteManifestPath);
|
||||
let extension = path.extname(this._absoluteManifestPath);
|
||||
let basename = this._absoluteManifestPath.substr(0, this._absoluteManifestPath.length - extension.length);
|
||||
|
||||
const translationId = `${extensionDescription.publisher}.${extensionDescription.name}`;
|
||||
let translationPath = this._nlsConfig.translations[translationId];
|
||||
let localizedMessages: TPromise<LocalizedMessages>;
|
||||
let localizedMessages: Promise<LocalizedMessages | undefined>;
|
||||
if (translationPath) {
|
||||
localizedMessages = pfs.readFile(translationPath, 'utf8').then<LocalizedMessages, LocalizedMessages>((content) => {
|
||||
let errors: json.ParseError[] = [];
|
||||
@@ -163,7 +161,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
|
||||
return { values: undefined, default: `${basename}.nls.json` };
|
||||
});
|
||||
} else {
|
||||
localizedMessages = pfs.fileExists(basename + '.nls' + extension).then<LocalizedMessages, undefined | LocalizedMessages>(exists => {
|
||||
localizedMessages = pfs.fileExists(basename + '.nls' + extension).then<LocalizedMessages | undefined, LocalizedMessages | undefined>(exists => {
|
||||
if (!exists) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -211,8 +209,8 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
|
||||
/**
|
||||
* Parses original message bundle, returns null if the original message bundle is null.
|
||||
*/
|
||||
private static resolveOriginalMessageBundle(originalMessageBundle: string, errors: json.ParseError[]) {
|
||||
return new TPromise<{ [key: string]: string; }>((c, e) => {
|
||||
private static resolveOriginalMessageBundle(originalMessageBundle: string | null, errors: json.ParseError[]) {
|
||||
return new Promise<{ [key: string]: string; } | null>((c, e) => {
|
||||
if (originalMessageBundle) {
|
||||
pfs.readFile(originalMessageBundle).then(originalBundleContent => {
|
||||
c(json.parse(originalBundleContent.toString(), errors));
|
||||
@@ -229,8 +227,8 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
|
||||
* Finds localized message bundle and the original (unlocalized) one.
|
||||
* If the localized file is not present, returns null for the original and marks original as localized.
|
||||
*/
|
||||
private static findMessageBundles(nlsConfig: NlsConfiguration, basename: string): TPromise<{ localized: string, original: string }> {
|
||||
return new TPromise<{ localized: string, original: string }>((c, e) => {
|
||||
private static findMessageBundles(nlsConfig: NlsConfiguration, basename: string): Promise<{ localized: string; original: string | null; }> {
|
||||
return new Promise<{ localized: string; original: string | null; }>((c, e) => {
|
||||
function loop(basename: string, locale: string): void {
|
||||
let toCheck = `${basename}.nls.${locale}.json`;
|
||||
pfs.fileExists(toCheck).then(exists => {
|
||||
@@ -258,7 +256,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
|
||||
* This routine makes the following assumptions:
|
||||
* The root element is an object literal
|
||||
*/
|
||||
private static _replaceNLStrings<T>(nlsConfig: NlsConfiguration, literal: T, messages: { [key: string]: string; }, originalMessages: { [key: string]: string }, log: ILog, messageScope: string): void {
|
||||
private static _replaceNLStrings<T>(nlsConfig: NlsConfiguration, literal: T, messages: { [key: string]: string; }, originalMessages: { [key: string]: string } | null, log: ILog, messageScope: string): void {
|
||||
function processEntry(obj: any, key: string | number, command?: boolean) {
|
||||
let value = obj[key];
|
||||
if (types.isString(value)) {
|
||||
@@ -320,7 +318,7 @@ export interface IRelaxedExtensionDescription {
|
||||
}
|
||||
|
||||
class ExtensionManifestValidator extends ExtensionManifestHandler {
|
||||
validate(_extensionDescription: IExtensionDescription): IExtensionDescription {
|
||||
validate(_extensionDescription: IExtensionDescription): IExtensionDescription | null {
|
||||
let extensionDescription = <IRelaxedExtensionDescription>_extensionDescription;
|
||||
extensionDescription.isBuiltin = this._isBuiltin;
|
||||
extensionDescription.isUnderDevelopment = this._isUnderDevelopment;
|
||||
@@ -338,12 +336,17 @@ class ExtensionManifestValidator extends ExtensionManifestHandler {
|
||||
this._log.warn(this._absoluteFolderPath, error);
|
||||
});
|
||||
|
||||
// allow publisher to be undefined to make the initial extension authoring experience smoother
|
||||
if (!extensionDescription.publisher) {
|
||||
extensionDescription.publisher = 'undefined_publisher';
|
||||
}
|
||||
|
||||
// id := `publisher.name`
|
||||
extensionDescription.id = `${extensionDescription.publisher}.${extensionDescription.name}`;
|
||||
|
||||
// main := absolutePath(`main`)
|
||||
if (extensionDescription.main) {
|
||||
extensionDescription.main = join(this._absoluteFolderPath, extensionDescription.main);
|
||||
extensionDescription.main = path.join(this._absoluteFolderPath, extensionDescription.main);
|
||||
}
|
||||
|
||||
extensionDescription.extensionLocation = URI.file(this._absoluteFolderPath);
|
||||
@@ -370,8 +373,8 @@ class ExtensionManifestValidator extends ExtensionManifestHandler {
|
||||
notices.push(nls.localize('extensionDescription.empty', "Got empty extension description"));
|
||||
return false;
|
||||
}
|
||||
if (typeof extensionDescription.publisher !== 'string') {
|
||||
notices.push(nls.localize('extensionDescription.publisher', "property `{0}` is mandatory and must be of type `string`", 'publisher'));
|
||||
if (typeof extensionDescription.publisher !== 'undefined' && typeof extensionDescription.publisher !== 'string') {
|
||||
notices.push(nls.localize('extensionDescription.publisher', "property publisher must be of type `string`."));
|
||||
return false;
|
||||
}
|
||||
if (typeof extensionDescription.name !== 'string') {
|
||||
@@ -411,7 +414,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler {
|
||||
notices.push(nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main'));
|
||||
return false;
|
||||
} else {
|
||||
let normalizedAbsolutePath = join(extensionFolderPath, extensionDescription.main);
|
||||
let normalizedAbsolutePath = path.join(extensionFolderPath, extensionDescription.main);
|
||||
|
||||
if (normalizedAbsolutePath.indexOf(extensionFolderPath)) {
|
||||
notices.push(nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", normalizedAbsolutePath, extensionFolderPath));
|
||||
@@ -445,8 +448,8 @@ export class ExtensionScannerInput {
|
||||
|
||||
constructor(
|
||||
public readonly ourVersion: string,
|
||||
public readonly commit: string,
|
||||
public readonly locale: string,
|
||||
public readonly commit: string | undefined,
|
||||
public readonly locale: string | undefined,
|
||||
public readonly devMode: boolean,
|
||||
public readonly absoluteFolderPath: string,
|
||||
public readonly isBuiltin: boolean,
|
||||
@@ -486,16 +489,16 @@ export interface IExtensionReference {
|
||||
}
|
||||
|
||||
export interface IExtensionResolver {
|
||||
resolveExtensions(): TPromise<IExtensionReference[]>;
|
||||
resolveExtensions(): Promise<IExtensionReference[]>;
|
||||
}
|
||||
|
||||
class DefaultExtensionResolver implements IExtensionResolver {
|
||||
|
||||
constructor(private root: string) { }
|
||||
|
||||
resolveExtensions(): TPromise<IExtensionReference[]> {
|
||||
resolveExtensions(): Promise<IExtensionReference[]> {
|
||||
return pfs.readDirsInDir(this.root)
|
||||
.then(folders => folders.map(name => ({ name, path: join(this.root, name) })));
|
||||
.then(folders => folders.map(name => ({ name, path: path.join(this.root, name) })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,11 +507,11 @@ export class ExtensionScanner {
|
||||
/**
|
||||
* Read the extension defined in `absoluteFolderPath`
|
||||
*/
|
||||
public static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): TPromise<IExtensionDescription> {
|
||||
absoluteFolderPath = normalize(absoluteFolderPath);
|
||||
public 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);
|
||||
return parser.parse().then((extensionDescription) => {
|
||||
return parser.parse().then<IExtensionDescription | null>((extensionDescription) => {
|
||||
if (extensionDescription === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -528,7 +531,7 @@ export class ExtensionScanner {
|
||||
/**
|
||||
* Scan a list of extensions defined in `absoluteFolderPath`
|
||||
*/
|
||||
public static async scanExtensions(input: ExtensionScannerInput, log: ILog, resolver: IExtensionResolver = null): Promise<IExtensionDescription[]> {
|
||||
public static async scanExtensions(input: ExtensionScannerInput, log: ILog, resolver: IExtensionResolver | null = null): Promise<IExtensionDescription[]> {
|
||||
const absoluteFolderPath = input.absoluteFolderPath;
|
||||
const isBuiltin = input.isBuiltin;
|
||||
const isUnderDevelopment = input.isUnderDevelopment;
|
||||
@@ -541,7 +544,7 @@ export class ExtensionScanner {
|
||||
let obsolete: { [folderName: string]: boolean; } = {};
|
||||
if (!isBuiltin) {
|
||||
try {
|
||||
const obsoleteFileContents = await pfs.readFile(join(absoluteFolderPath, '.obsolete'), 'utf8');
|
||||
const obsoleteFileContents = await pfs.readFile(path.join(absoluteFolderPath, '.obsolete'), 'utf8');
|
||||
obsolete = JSON.parse(obsoleteFileContents);
|
||||
} catch (err) {
|
||||
// Don't care
|
||||
@@ -572,7 +575,8 @@ export class ExtensionScanner {
|
||||
}
|
||||
|
||||
const nlsConfig = ExtensionScannerInput.createNLSConfig(input);
|
||||
let extensionDescriptions = await TPromise.join(refs.map(r => this.scanExtension(input.ourVersion, log, r.path, isBuiltin, isUnderDevelopment, nlsConfig)));
|
||||
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)]);
|
||||
|
||||
if (!isBuiltin) {
|
||||
@@ -598,12 +602,12 @@ export class ExtensionScanner {
|
||||
* Combination of scanExtension and scanExtensions: If an extension manifest is found at root, we load just this extension,
|
||||
* otherwise we assume the folder contains multiple extensions.
|
||||
*/
|
||||
public static scanOneOrMultipleExtensions(input: ExtensionScannerInput, log: ILog): TPromise<IExtensionDescription[]> {
|
||||
public static scanOneOrMultipleExtensions(input: ExtensionScannerInput, log: ILog): Promise<IExtensionDescription[]> {
|
||||
const absoluteFolderPath = input.absoluteFolderPath;
|
||||
const isBuiltin = input.isBuiltin;
|
||||
const isUnderDevelopment = input.isUnderDevelopment;
|
||||
|
||||
return pfs.fileExists(join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => {
|
||||
return pfs.fileExists(path.join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => {
|
||||
if (exists) {
|
||||
const nlsConfig = ExtensionScannerInput.createNLSConfig(input);
|
||||
return this.scanExtension(input.ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig).then((extensionDescription) => {
|
||||
@@ -619,4 +623,31 @@ export class ExtensionScanner {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
// Overwrite with extensions found in extra
|
||||
for (let i = 0, len = extraBuiltinExtensions.length; i < len; i++) {
|
||||
resultMap[extraBuiltinExtensions[i].id] = extraBuiltinExtensions[i];
|
||||
}
|
||||
|
||||
let resultArr = Object.keys(resultMap).map((id) => resultMap[id]);
|
||||
resultArr.sort((a, b) => {
|
||||
const aLastSegment = path.basename(a.extensionLocation.fsPath);
|
||||
const bLastSegment = path.basename(b.extensionLocation.fsPath);
|
||||
if (aLastSegment < bLastSegment) {
|
||||
return -1;
|
||||
}
|
||||
if (aLastSegment > bLastSegment) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return resultArr;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,14 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TPromise, ValueCallback, ErrorCallback } from 'vs/base/common/winjs.base';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
|
||||
export class LazyPromise implements TPromise<any> {
|
||||
export class LazyPromise implements Thenable<any> {
|
||||
|
||||
private _onCancel: () => void;
|
||||
|
||||
private _actual: TPromise<any>;
|
||||
private _actualOk: ValueCallback;
|
||||
private _actualErr: ErrorCallback;
|
||||
private _actual: Promise<any> | null;
|
||||
private _actualOk: ((value?: any) => any) | null;
|
||||
private _actualErr: ((err?: any) => any) | null;
|
||||
|
||||
private _hasValue: boolean;
|
||||
private _value: any;
|
||||
@@ -21,10 +17,7 @@ export class LazyPromise implements TPromise<any> {
|
||||
private _hasErr: boolean;
|
||||
private _err: any;
|
||||
|
||||
private _isCanceled: boolean;
|
||||
|
||||
constructor(onCancel: () => void) {
|
||||
this._onCancel = onCancel;
|
||||
constructor() {
|
||||
this._actual = null;
|
||||
this._actualOk = null;
|
||||
this._actualErr = null;
|
||||
@@ -32,29 +25,28 @@ export class LazyPromise implements TPromise<any> {
|
||||
this._value = null;
|
||||
this._hasErr = false;
|
||||
this._err = null;
|
||||
this._isCanceled = false;
|
||||
}
|
||||
|
||||
private _ensureActual(): TPromise<any> {
|
||||
private _ensureActual(): Promise<any> {
|
||||
if (!this._actual) {
|
||||
this._actual = new TPromise<any>((c, e) => {
|
||||
this._actual = new Promise<any>((c, e) => {
|
||||
this._actualOk = c;
|
||||
this._actualErr = e;
|
||||
}, this._onCancel);
|
||||
|
||||
if (this._hasValue) {
|
||||
this._actualOk(this._value);
|
||||
}
|
||||
if (this._hasValue) {
|
||||
this._actualOk(this._value);
|
||||
}
|
||||
|
||||
if (this._hasErr) {
|
||||
this._actualErr(this._err);
|
||||
}
|
||||
if (this._hasErr) {
|
||||
this._actualErr(this._err);
|
||||
}
|
||||
});
|
||||
}
|
||||
return this._actual;
|
||||
}
|
||||
|
||||
public resolveOk(value: any): void {
|
||||
if (this._isCanceled || this._hasErr) {
|
||||
if (this._hasValue || this._hasErr) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,12 +54,12 @@ export class LazyPromise implements TPromise<any> {
|
||||
this._value = value;
|
||||
|
||||
if (this._actual) {
|
||||
this._actualOk(value);
|
||||
this._actualOk!(value);
|
||||
}
|
||||
}
|
||||
|
||||
public resolveErr(err: any): void {
|
||||
if (this._isCanceled || this._hasValue) {
|
||||
if (this._hasValue || this._hasErr) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +67,7 @@ export class LazyPromise implements TPromise<any> {
|
||||
this._err = err;
|
||||
|
||||
if (this._actual) {
|
||||
this._actualErr(err);
|
||||
this._actualErr!(err);
|
||||
} else {
|
||||
// If nobody's listening at this point, it is safe to assume they never will,
|
||||
// since resolving this promise is always "async"
|
||||
@@ -84,32 +76,6 @@ export class LazyPromise implements TPromise<any> {
|
||||
}
|
||||
|
||||
public then(success: any, error: any): any {
|
||||
if (this._isCanceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._ensureActual().then(success, error);
|
||||
}
|
||||
|
||||
public done(success: any, error: any): void {
|
||||
if (this._isCanceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._ensureActual().done(success, error);
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
if (this._hasValue || this._hasErr) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isCanceled = true;
|
||||
|
||||
if (this._actual) {
|
||||
this._actual.cancel();
|
||||
} else {
|
||||
this._onCancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
export interface IRPCProtocol {
|
||||
/**
|
||||
@@ -22,27 +21,35 @@ export interface IRPCProtocol {
|
||||
}
|
||||
|
||||
export class ProxyIdentifier<T> {
|
||||
public static count = 0;
|
||||
_proxyIdentifierBrand: void;
|
||||
_suppressCompilerUnusedWarning: T;
|
||||
|
||||
public readonly isMain: boolean;
|
||||
public readonly id: string;
|
||||
public readonly sid: string;
|
||||
public readonly nid: number;
|
||||
|
||||
constructor(isMain: boolean, id: string) {
|
||||
constructor(isMain: boolean, sid: string) {
|
||||
this.isMain = isMain;
|
||||
this.id = id;
|
||||
this.sid = sid;
|
||||
this.nid = (++ProxyIdentifier.count);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Using `isFancy` indicates that arguments or results of type `URI` or `RegExp`
|
||||
* will be serialized/deserialized automatically, but this has a performance cost,
|
||||
* as each argument/result must be visited.
|
||||
*/
|
||||
const identifiers: ProxyIdentifier<any>[] = [];
|
||||
|
||||
export function createMainContextProxyIdentifier<T>(identifier: string): ProxyIdentifier<T> {
|
||||
return new ProxyIdentifier(true, 'm' + identifier);
|
||||
const result = new ProxyIdentifier<T>(true, identifier);
|
||||
identifiers[result.nid] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function createExtHostContextProxyIdentifier<T>(identifier: string): ProxyIdentifier<T> {
|
||||
return new ProxyIdentifier(false, 'e' + identifier);
|
||||
const result = new ProxyIdentifier<T>(false, identifier);
|
||||
identifiers[result.nid] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getStringIdentifierForProxy(nid: number): string {
|
||||
return identifiers[nid].sid;
|
||||
}
|
||||
|
||||
@@ -2,19 +2,43 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { LazyPromise } from 'vs/workbench/services/extensions/node/lazyPromise';
|
||||
import { ProxyIdentifier, IRPCProtocol } from 'vs/workbench/services/extensions/node/proxyIdentifier';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import URI from 'vs/base/common/uri';
|
||||
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 { 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';
|
||||
|
||||
declare var Proxy: any; // TODO@TypeScript
|
||||
export interface JSONStringifyReplacer {
|
||||
(key: string, value: any): any;
|
||||
}
|
||||
|
||||
function safeStringify(obj: any, replacer: JSONStringifyReplacer | null): string {
|
||||
try {
|
||||
return JSON.stringify(obj, <(key: string, value: any) => any>replacer);
|
||||
} catch (err) {
|
||||
return 'null';
|
||||
}
|
||||
}
|
||||
|
||||
function createURIReplacer(transformer: IURITransformer | null): JSONStringifyReplacer | null {
|
||||
if (!transformer) {
|
||||
return null;
|
||||
}
|
||||
return (key: string, value: any): any => {
|
||||
if (value && value.$mid === 1) {
|
||||
return transformer.transformOutgoing(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
function _transformOutgoingURIs(obj: any, transformer: IURITransformer, depth: number): any {
|
||||
|
||||
@@ -41,7 +65,7 @@ function _transformOutgoingURIs(obj: any, transformer: IURITransformer, depth: n
|
||||
return null;
|
||||
}
|
||||
|
||||
export function transformOutgoingURIs(obj: any, transformer: IURITransformer): any {
|
||||
export function transformOutgoingURIs<T>(obj: T, transformer: IURITransformer): T {
|
||||
const result = _transformOutgoingURIs(obj, transformer, 0);
|
||||
if (result === null) {
|
||||
// no change
|
||||
@@ -76,7 +100,7 @@ function _transformIncomingURIs(obj: any, transformer: IURITransformer, depth: n
|
||||
return null;
|
||||
}
|
||||
|
||||
function transformIncomingURIs(obj: any, transformer: IURITransformer): any {
|
||||
function transformIncomingURIs<T>(obj: T, transformer: IURITransformer): T {
|
||||
const result = _transformIncomingURIs(obj, transformer, 0);
|
||||
if (result === null) {
|
||||
// no change
|
||||
@@ -85,26 +109,66 @@ function transformIncomingURIs(obj: any, transformer: IURITransformer): any {
|
||||
return result;
|
||||
}
|
||||
|
||||
export class RPCProtocol implements IRPCProtocol {
|
||||
export const enum RequestInitiator {
|
||||
LocalSide = 0,
|
||||
OtherSide = 1
|
||||
}
|
||||
|
||||
private readonly _uriTransformer: IURITransformer;
|
||||
export const enum ResponsiveState {
|
||||
Responsive = 0,
|
||||
Unresponsive = 1
|
||||
}
|
||||
|
||||
export interface IRPCProtocolLogger {
|
||||
logIncoming(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void;
|
||||
logOutgoing(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void;
|
||||
}
|
||||
|
||||
const noop = () => { };
|
||||
|
||||
export class RPCProtocol extends Disposable implements IRPCProtocol {
|
||||
|
||||
private static UNRESPONSIVE_TIME = 3 * 1000; // 3s
|
||||
|
||||
private readonly _onDidChangeResponsiveState: Emitter<ResponsiveState> = this._register(new Emitter<ResponsiveState>());
|
||||
public readonly onDidChangeResponsiveState: Event<ResponsiveState> = this._onDidChangeResponsiveState.event;
|
||||
|
||||
private readonly _protocol: IMessagePassingProtocol;
|
||||
private readonly _logger: IRPCProtocolLogger | null;
|
||||
private readonly _uriTransformer: IURITransformer | null;
|
||||
private readonly _uriReplacer: JSONStringifyReplacer | null;
|
||||
private _isDisposed: boolean;
|
||||
private readonly _locals: { [id: string]: any; };
|
||||
private readonly _proxies: { [id: string]: any; };
|
||||
private readonly _locals: any[];
|
||||
private readonly _proxies: any[];
|
||||
private _lastMessageId: number;
|
||||
private readonly _invokedHandlers: { [req: string]: TPromise<any>; };
|
||||
private readonly _cancelInvokedHandlers: { [req: string]: () => void; };
|
||||
private readonly _pendingRPCReplies: { [msgId: string]: LazyPromise; };
|
||||
private readonly _multiplexor: RPCMultiplexer;
|
||||
private _responsiveState: ResponsiveState;
|
||||
private _unacknowledgedCount: number;
|
||||
private _unresponsiveTime: number;
|
||||
private _asyncCheckUresponsive: RunOnceScheduler;
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol, transformer: IURITransformer = null) {
|
||||
constructor(protocol: IMessagePassingProtocol, logger: IRPCProtocolLogger | null = null, transformer: IURITransformer | null = null) {
|
||||
super();
|
||||
this._protocol = protocol;
|
||||
this._logger = logger;
|
||||
this._uriTransformer = transformer;
|
||||
this._uriReplacer = createURIReplacer(this._uriTransformer);
|
||||
this._isDisposed = false;
|
||||
this._locals = Object.create(null);
|
||||
this._proxies = Object.create(null);
|
||||
this._locals = [];
|
||||
this._proxies = [];
|
||||
for (let i = 0, len = ProxyIdentifier.count; i < len; i++) {
|
||||
this._locals[i] = null;
|
||||
this._proxies[i] = null;
|
||||
}
|
||||
this._lastMessageId = 0;
|
||||
this._invokedHandlers = Object.create(null);
|
||||
this._cancelInvokedHandlers = Object.create(null);
|
||||
this._pendingRPCReplies = {};
|
||||
this._multiplexor = new RPCMultiplexer(protocol, (msg) => this._receiveOneMessage(msg));
|
||||
this._responsiveState = ResponsiveState.Responsive;
|
||||
this._unacknowledgedCount = 0;
|
||||
this._unresponsiveTime = 0;
|
||||
this._asyncCheckUresponsive = this._register(new RunOnceScheduler(() => this._checkUnresponsive(), 1000));
|
||||
this._protocol.onMessage((msg) => this._receiveOneMessage(msg));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
@@ -117,6 +181,58 @@ export class RPCProtocol implements IRPCProtocol {
|
||||
});
|
||||
}
|
||||
|
||||
private _onWillSendRequest(req: number): void {
|
||||
if (this._unacknowledgedCount === 0) {
|
||||
// Since this is the first request we are sending in a while,
|
||||
// mark this moment as the start for the countdown to unresponsive time
|
||||
this._unresponsiveTime = Date.now() + RPCProtocol.UNRESPONSIVE_TIME;
|
||||
}
|
||||
this._unacknowledgedCount++;
|
||||
if (!this._asyncCheckUresponsive.isScheduled()) {
|
||||
this._asyncCheckUresponsive.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
private _onDidReceiveAcknowledge(req: number): void {
|
||||
// The next possible unresponsive time is now + delta.
|
||||
this._unresponsiveTime = Date.now() + RPCProtocol.UNRESPONSIVE_TIME;
|
||||
this._unacknowledgedCount--;
|
||||
if (this._unacknowledgedCount === 0) {
|
||||
// No more need to check for unresponsive
|
||||
this._asyncCheckUresponsive.cancel();
|
||||
}
|
||||
// The ext host is responsive!
|
||||
this._setResponsiveState(ResponsiveState.Responsive);
|
||||
}
|
||||
|
||||
private _checkUnresponsive(): void {
|
||||
if (this._unacknowledgedCount === 0) {
|
||||
// Not waiting for anything => cannot say if it is responsive or not
|
||||
return;
|
||||
}
|
||||
|
||||
if (Date.now() > this._unresponsiveTime) {
|
||||
// Unresponsive!!
|
||||
this._setResponsiveState(ResponsiveState.Unresponsive);
|
||||
} else {
|
||||
// Not (yet) unresponsive, be sure to check again soon
|
||||
this._asyncCheckUresponsive.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
private _setResponsiveState(newResponsiveState: ResponsiveState): void {
|
||||
if (this._responsiveState === newResponsiveState) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
this._responsiveState = newResponsiveState;
|
||||
this._onDidChangeResponsiveState.fire(this._responsiveState);
|
||||
}
|
||||
|
||||
public get responsiveState(): ResponsiveState {
|
||||
return this._responsiveState;
|
||||
}
|
||||
|
||||
public transformIncomingURIs<T>(obj: T): T {
|
||||
if (!this._uriTransformer) {
|
||||
return obj;
|
||||
@@ -125,18 +241,19 @@ export class RPCProtocol implements IRPCProtocol {
|
||||
}
|
||||
|
||||
public getProxy<T>(identifier: ProxyIdentifier<T>): T {
|
||||
if (!this._proxies[identifier.id]) {
|
||||
this._proxies[identifier.id] = this._createProxy(identifier.id);
|
||||
const rpcId = identifier.nid;
|
||||
if (!this._proxies[rpcId]) {
|
||||
this._proxies[rpcId] = this._createProxy(rpcId);
|
||||
}
|
||||
return this._proxies[identifier.id];
|
||||
return this._proxies[rpcId];
|
||||
}
|
||||
|
||||
private _createProxy<T>(proxyId: string): T {
|
||||
private _createProxy<T>(rpcId: number): T {
|
||||
let handler = {
|
||||
get: (target: any, name: string) => {
|
||||
if (!target[name] && name.charCodeAt(0) === CharCode.DollarSign) {
|
||||
target[name] = (...myArgs: any[]) => {
|
||||
return this._remoteCall(proxyId, name, myArgs);
|
||||
return this._remoteCall(rpcId, name, myArgs);
|
||||
};
|
||||
}
|
||||
return target[name];
|
||||
@@ -146,72 +263,151 @@ export class RPCProtocol implements IRPCProtocol {
|
||||
}
|
||||
|
||||
public set<T, R extends T>(identifier: ProxyIdentifier<T>, value: R): R {
|
||||
this._locals[identifier.id] = value;
|
||||
this._locals[identifier.nid] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
public assertRegistered(identifiers: ProxyIdentifier<any>[]): void {
|
||||
for (let i = 0, len = identifiers.length; i < len; i++) {
|
||||
const identifier = identifiers[i];
|
||||
if (!this._locals[identifier.id]) {
|
||||
throw new Error(`Missing actor ${identifier.id} (isMain: ${identifier.isMain})`);
|
||||
if (!this._locals[identifier.nid]) {
|
||||
throw new Error(`Missing actor ${identifier.sid} (isMain: ${identifier.isMain})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _receiveOneMessage(rawmsg: string): void {
|
||||
private _receiveOneMessage(rawmsg: Buffer): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = <RPCMessage>JSON.parse(rawmsg);
|
||||
if (this._uriTransformer) {
|
||||
msg = transformIncomingURIs(msg, this._uriTransformer);
|
||||
}
|
||||
const msgLength = rawmsg.length;
|
||||
const buff = MessageBuffer.read(rawmsg, 0);
|
||||
const messageType = <MessageType>buff.readUInt8();
|
||||
const req = buff.readUInt32();
|
||||
|
||||
switch (msg.type) {
|
||||
case MessageType.Request:
|
||||
this._receiveRequest(msg);
|
||||
switch (messageType) {
|
||||
case MessageType.RequestJSONArgs:
|
||||
case MessageType.RequestJSONArgsWithCancellation: {
|
||||
let { rpcId, method, args } = MessageIO.deserializeRequestJSONArgs(buff);
|
||||
if (this._uriTransformer) {
|
||||
args = transformIncomingURIs(args, this._uriTransformer);
|
||||
}
|
||||
this._receiveRequest(msgLength, req, rpcId, method, args, (messageType === MessageType.RequestJSONArgsWithCancellation));
|
||||
break;
|
||||
case MessageType.Cancel:
|
||||
this._receiveCancel(msg);
|
||||
}
|
||||
case MessageType.RequestMixedArgs:
|
||||
case MessageType.RequestMixedArgsWithCancellation: {
|
||||
let { rpcId, method, args } = MessageIO.deserializeRequestMixedArgs(buff);
|
||||
if (this._uriTransformer) {
|
||||
args = transformIncomingURIs(args, this._uriTransformer);
|
||||
}
|
||||
this._receiveRequest(msgLength, req, rpcId, method, args, (messageType === MessageType.RequestMixedArgsWithCancellation));
|
||||
break;
|
||||
case MessageType.Reply:
|
||||
this._receiveReply(msg);
|
||||
}
|
||||
case MessageType.Acknowledged: {
|
||||
if (this._logger) {
|
||||
this._logger.logIncoming(msgLength, req, RequestInitiator.LocalSide, `ack`);
|
||||
}
|
||||
this._onDidReceiveAcknowledge(req);
|
||||
break;
|
||||
case MessageType.ReplyErr:
|
||||
this._receiveReplyErr(msg);
|
||||
}
|
||||
case MessageType.Cancel: {
|
||||
this._receiveCancel(msgLength, req);
|
||||
break;
|
||||
}
|
||||
case MessageType.ReplyOKEmpty: {
|
||||
this._receiveReply(msgLength, req, undefined);
|
||||
break;
|
||||
}
|
||||
case MessageType.ReplyOKJSON: {
|
||||
let value = MessageIO.deserializeReplyOKJSON(buff);
|
||||
if (this._uriTransformer) {
|
||||
value = transformIncomingURIs(value, this._uriTransformer);
|
||||
}
|
||||
this._receiveReply(msgLength, req, value);
|
||||
break;
|
||||
}
|
||||
case MessageType.ReplyOKBuffer: {
|
||||
let value = MessageIO.deserializeReplyOKBuffer(buff);
|
||||
this._receiveReply(msgLength, req, value);
|
||||
break;
|
||||
}
|
||||
case MessageType.ReplyErrError: {
|
||||
let err = MessageIO.deserializeReplyErrError(buff);
|
||||
if (this._uriTransformer) {
|
||||
err = transformIncomingURIs(err, this._uriTransformer);
|
||||
}
|
||||
this._receiveReplyErr(msgLength, req, err);
|
||||
break;
|
||||
}
|
||||
case MessageType.ReplyErrEmpty: {
|
||||
this._receiveReplyErr(msgLength, req, undefined);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _receiveRequest(msg: RequestMessage): void {
|
||||
const callId = msg.id;
|
||||
const proxyId = msg.proxyId;
|
||||
private _receiveRequest(msgLength: number, req: number, rpcId: number, method: string, args: any[], usesCancellationToken: boolean): void {
|
||||
if (this._logger) {
|
||||
this._logger.logIncoming(msgLength, req, RequestInitiator.OtherSide, `receiveRequest ${getStringIdentifierForProxy(rpcId)}.${method}(`, args);
|
||||
}
|
||||
const callId = String(req);
|
||||
|
||||
this._invokedHandlers[callId] = this._invokeHandler(proxyId, msg.method, msg.args);
|
||||
let promise: Thenable<any>;
|
||||
let cancel: () => void;
|
||||
if (usesCancellationToken) {
|
||||
const cancellationTokenSource = new CancellationTokenSource();
|
||||
args.push(cancellationTokenSource.token);
|
||||
promise = this._invokeHandler(rpcId, method, args);
|
||||
cancel = () => cancellationTokenSource.cancel();
|
||||
} else {
|
||||
// cannot be cancelled
|
||||
promise = this._invokeHandler(rpcId, method, args);
|
||||
cancel = noop;
|
||||
}
|
||||
|
||||
this._invokedHandlers[callId].then((r) => {
|
||||
delete this._invokedHandlers[callId];
|
||||
if (this._uriTransformer) {
|
||||
r = transformOutgoingURIs(r, this._uriTransformer);
|
||||
this._cancelInvokedHandlers[callId] = cancel;
|
||||
|
||||
// Acknowledge the request
|
||||
const msg = MessageIO.serializeAcknowledged(req);
|
||||
if (this._logger) {
|
||||
this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.OtherSide, `ack`);
|
||||
}
|
||||
this._protocol.send(msg);
|
||||
|
||||
promise.then((r) => {
|
||||
delete this._cancelInvokedHandlers[callId];
|
||||
const msg = MessageIO.serializeReplyOK(req, r, this._uriReplacer);
|
||||
if (this._logger) {
|
||||
this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.OtherSide, `reply:`, r);
|
||||
}
|
||||
this._multiplexor.send(MessageFactory.replyOK(callId, r));
|
||||
this._protocol.send(msg);
|
||||
}, (err) => {
|
||||
delete this._invokedHandlers[callId];
|
||||
this._multiplexor.send(MessageFactory.replyErr(callId, err));
|
||||
delete this._cancelInvokedHandlers[callId];
|
||||
const msg = MessageIO.serializeReplyErr(req, err);
|
||||
if (this._logger) {
|
||||
this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.OtherSide, `replyErr:`, err);
|
||||
}
|
||||
this._protocol.send(msg);
|
||||
});
|
||||
}
|
||||
|
||||
private _receiveCancel(msg: CancelMessage): void {
|
||||
const callId = msg.id;
|
||||
if (this._invokedHandlers[callId]) {
|
||||
this._invokedHandlers[callId].cancel();
|
||||
private _receiveCancel(msgLength: number, req: number): void {
|
||||
if (this._logger) {
|
||||
this._logger.logIncoming(msgLength, req, RequestInitiator.OtherSide, `receiveCancel`);
|
||||
}
|
||||
const callId = String(req);
|
||||
if (this._cancelInvokedHandlers[callId]) {
|
||||
this._cancelInvokedHandlers[callId]();
|
||||
}
|
||||
}
|
||||
|
||||
private _receiveReply(msg: ReplyMessage): void {
|
||||
const callId = msg.id;
|
||||
private _receiveReply(msgLength: number, req: number, value: any): void {
|
||||
if (this._logger) {
|
||||
this._logger.logIncoming(msgLength, req, RequestInitiator.LocalSide, `receiveReply:`, value);
|
||||
}
|
||||
const callId = String(req);
|
||||
if (!this._pendingRPCReplies.hasOwnProperty(callId)) {
|
||||
return;
|
||||
}
|
||||
@@ -219,11 +415,15 @@ export class RPCProtocol implements IRPCProtocol {
|
||||
const pendingReply = this._pendingRPCReplies[callId];
|
||||
delete this._pendingRPCReplies[callId];
|
||||
|
||||
pendingReply.resolveOk(msg.res);
|
||||
pendingReply.resolveOk(value);
|
||||
}
|
||||
|
||||
private _receiveReplyErr(msg: ReplyErrMessage): void {
|
||||
const callId = msg.id;
|
||||
private _receiveReplyErr(msgLength: number, req: number, value: any): void {
|
||||
if (this._logger) {
|
||||
this._logger.logIncoming(msgLength, req, RequestInitiator.LocalSide, `receiveReplyErr:`, value);
|
||||
}
|
||||
|
||||
const callId = String(req);
|
||||
if (!this._pendingRPCReplies.hasOwnProperty(callId)) {
|
||||
return;
|
||||
}
|
||||
@@ -231,145 +431,402 @@ export class RPCProtocol implements IRPCProtocol {
|
||||
const pendingReply = this._pendingRPCReplies[callId];
|
||||
delete this._pendingRPCReplies[callId];
|
||||
|
||||
let err: Error = null;
|
||||
if (msg.err && msg.err.$isError) {
|
||||
let err: Error | null = null;
|
||||
if (value && value.$isError) {
|
||||
err = new Error();
|
||||
err.name = msg.err.name;
|
||||
err.message = msg.err.message;
|
||||
err.stack = msg.err.stack;
|
||||
err.name = value.name;
|
||||
err.message = value.message;
|
||||
err.stack = value.stack;
|
||||
}
|
||||
pendingReply.resolveErr(err);
|
||||
}
|
||||
|
||||
private _invokeHandler(proxyId: string, methodName: string, args: any[]): TPromise<any> {
|
||||
private _invokeHandler(rpcId: number, methodName: string, args: any[]): Thenable<any> {
|
||||
try {
|
||||
return TPromise.as(this._doInvokeHandler(proxyId, methodName, args));
|
||||
return Promise.resolve(this._doInvokeHandler(rpcId, methodName, args));
|
||||
} catch (err) {
|
||||
return TPromise.wrapError(err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
private _doInvokeHandler(proxyId: string, methodName: string, args: any[]): any {
|
||||
if (!this._locals[proxyId]) {
|
||||
throw new Error('Unknown actor ' + proxyId);
|
||||
private _doInvokeHandler(rpcId: number, methodName: string, args: any[]): any {
|
||||
const actor = this._locals[rpcId];
|
||||
if (!actor) {
|
||||
throw new Error('Unknown actor ' + getStringIdentifierForProxy(rpcId));
|
||||
}
|
||||
let actor = this._locals[proxyId];
|
||||
let method = actor[methodName];
|
||||
if (typeof method !== 'function') {
|
||||
throw new Error('Unknown method ' + methodName + ' on actor ' + proxyId);
|
||||
throw new Error('Unknown method ' + methodName + ' on actor ' + getStringIdentifierForProxy(rpcId));
|
||||
}
|
||||
return method.apply(actor, args);
|
||||
}
|
||||
|
||||
private _remoteCall(proxyId: string, methodName: string, args: any[]): TPromise<any> {
|
||||
private _remoteCall(rpcId: number, methodName: string, args: any[]): Thenable<any> {
|
||||
if (this._isDisposed) {
|
||||
return TPromise.wrapError<any>(errors.canceled());
|
||||
return Promise.reject<any>(errors.canceled());
|
||||
}
|
||||
let cancellationToken: CancellationToken | null = null;
|
||||
if (args.length > 0 && CancellationToken.isCancellationToken(args[args.length - 1])) {
|
||||
cancellationToken = args.pop();
|
||||
}
|
||||
|
||||
const callId = String(++this._lastMessageId);
|
||||
const result = new LazyPromise(() => {
|
||||
this._multiplexor.send(MessageFactory.cancel(callId));
|
||||
});
|
||||
if (cancellationToken && cancellationToken.isCancellationRequested) {
|
||||
// No need to do anything...
|
||||
return Promise.reject<any>(errors.canceled());
|
||||
}
|
||||
|
||||
const req = ++this._lastMessageId;
|
||||
const callId = String(req);
|
||||
const result = new LazyPromise();
|
||||
|
||||
if (cancellationToken) {
|
||||
cancellationToken.onCancellationRequested(() => {
|
||||
const msg = MessageIO.serializeCancel(req);
|
||||
if (this._logger) {
|
||||
this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.LocalSide, `cancel`);
|
||||
}
|
||||
this._protocol.send(MessageIO.serializeCancel(req));
|
||||
});
|
||||
}
|
||||
|
||||
this._pendingRPCReplies[callId] = result;
|
||||
if (this._uriTransformer) {
|
||||
args = transformOutgoingURIs(args, this._uriTransformer);
|
||||
this._onWillSendRequest(req);
|
||||
const msg = MessageIO.serializeRequest(req, rpcId, methodName, args, !!cancellationToken, this._uriReplacer);
|
||||
if (this._logger) {
|
||||
this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.LocalSide, `request: ${getStringIdentifierForProxy(rpcId)}.${methodName}(`, args);
|
||||
}
|
||||
this._multiplexor.send(MessageFactory.request(callId, proxyId, methodName, args));
|
||||
this._protocol.send(msg);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends/Receives multiple messages in one go:
|
||||
* - multiple messages to be sent from one stack get sent in bulk at `process.nextTick`.
|
||||
* - each incoming message is handled in a separate `process.nextTick`.
|
||||
*/
|
||||
class RPCMultiplexer {
|
||||
class MessageBuffer {
|
||||
|
||||
private readonly _protocol: IMessagePassingProtocol;
|
||||
private readonly _sendAccumulatedBound: () => void;
|
||||
public static alloc(type: MessageType, req: number, messageSize: number): MessageBuffer {
|
||||
let result = new MessageBuffer(Buffer.allocUnsafe(messageSize + 1 /* type */ + 4 /* req */), 0);
|
||||
result.writeUInt8(type);
|
||||
result.writeUInt32(req);
|
||||
return result;
|
||||
}
|
||||
|
||||
private _messagesToSend: string[];
|
||||
public static read(buff: Buffer, offset: number): MessageBuffer {
|
||||
return new MessageBuffer(buff, offset);
|
||||
}
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol, onMessage: (msg: string) => void) {
|
||||
this._protocol = protocol;
|
||||
this._sendAccumulatedBound = this._sendAccumulated.bind(this);
|
||||
private _buff: Buffer;
|
||||
private _offset: number;
|
||||
|
||||
this._messagesToSend = [];
|
||||
public get buffer(): Buffer {
|
||||
return this._buff;
|
||||
}
|
||||
|
||||
this._protocol.onMessage(data => {
|
||||
for (let i = 0, len = data.length; i < len; i++) {
|
||||
onMessage(data[i]);
|
||||
private constructor(buff: Buffer, offset: number) {
|
||||
this._buff = buff;
|
||||
this._offset = offset;
|
||||
}
|
||||
|
||||
public static sizeUInt8(): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public writeUInt8(n: number): void {
|
||||
this._buff.writeUInt8(n, this._offset, true); this._offset += 1;
|
||||
}
|
||||
|
||||
public readUInt8(): number {
|
||||
const n = this._buff.readUInt8(this._offset, true); this._offset += 1;
|
||||
return n;
|
||||
}
|
||||
|
||||
public writeUInt32(n: number): void {
|
||||
this._buff.writeUInt32BE(n, this._offset, true); this._offset += 4;
|
||||
}
|
||||
|
||||
public readUInt32(): number {
|
||||
const n = this._buff.readUInt32BE(this._offset, true); this._offset += 4;
|
||||
return n;
|
||||
}
|
||||
|
||||
public static sizeShortString(str: string, strByteLength: number): number {
|
||||
return 1 /* string length */ + strByteLength /* actual string */;
|
||||
}
|
||||
|
||||
public writeShortString(str: string, strByteLength: number): void {
|
||||
this._buff.writeUInt8(strByteLength, this._offset, true); this._offset += 1;
|
||||
this._buff.write(str, this._offset, strByteLength, 'utf8'); this._offset += strByteLength;
|
||||
}
|
||||
|
||||
public readShortString(): string {
|
||||
const strLength = this._buff.readUInt8(this._offset, true); this._offset += 1;
|
||||
const str = this._buff.toString('utf8', this._offset, this._offset + strLength); this._offset += strLength;
|
||||
return str;
|
||||
}
|
||||
|
||||
public static sizeLongString(str: string, strByteLength: number): number {
|
||||
return 4 /* string length */ + strByteLength /* actual string */;
|
||||
}
|
||||
|
||||
public writeLongString(str: string, strByteLength: number): void {
|
||||
this._buff.writeUInt32LE(strByteLength, this._offset, true); this._offset += 4;
|
||||
this._buff.write(str, this._offset, strByteLength, 'utf8'); this._offset += strByteLength;
|
||||
}
|
||||
|
||||
public readLongString(): string {
|
||||
const strLength = this._buff.readUInt32LE(this._offset, true); this._offset += 4;
|
||||
const str = this._buff.toString('utf8', this._offset, this._offset + strLength); this._offset += strLength;
|
||||
return str;
|
||||
}
|
||||
|
||||
public static sizeBuffer(buff: Buffer, buffByteLength: number): number {
|
||||
return 4 /* buffer length */ + buffByteLength /* actual buffer */;
|
||||
}
|
||||
|
||||
public writeBuffer(buff: Buffer, buffByteLength: number): void {
|
||||
this._buff.writeUInt32LE(buffByteLength, this._offset, true); this._offset += 4;
|
||||
buff.copy(this._buff, this._offset); this._offset += buffByteLength;
|
||||
}
|
||||
|
||||
public readBuffer(): Buffer {
|
||||
const buffLength = this._buff.readUInt32LE(this._offset, true); this._offset += 4;
|
||||
const buff = this._buff.slice(this._offset, this._offset + buffLength); this._offset += buffLength;
|
||||
return buff;
|
||||
}
|
||||
|
||||
public static sizeMixedArray(arr: (string | Buffer)[], arrLengths: number[]): number {
|
||||
let size = 0;
|
||||
size += 1; // arr length
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
const el = arr[i];
|
||||
const elLength = arrLengths[i];
|
||||
size += 1; // arg type
|
||||
if (typeof el === 'string') {
|
||||
size += this.sizeLongString(el, elLength);
|
||||
} else {
|
||||
size += this.sizeBuffer(el, elLength);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _sendAccumulated(): void {
|
||||
const tmp = this._messagesToSend;
|
||||
this._messagesToSend = [];
|
||||
this._protocol.send(tmp);
|
||||
}
|
||||
|
||||
public send(msg: string): void {
|
||||
if (this._messagesToSend.length === 0) {
|
||||
process.nextTick(this._sendAccumulatedBound);
|
||||
}
|
||||
this._messagesToSend.push(msg);
|
||||
return size;
|
||||
}
|
||||
|
||||
public writeMixedArray(arr: (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];
|
||||
const elLength = arrLengths[i];
|
||||
if (typeof el === 'string') {
|
||||
this.writeUInt8(ArgType.String);
|
||||
this.writeLongString(el, elLength);
|
||||
} else {
|
||||
this.writeUInt8(ArgType.Buffer);
|
||||
this.writeBuffer(el, elLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readMixedArray(): (string | Buffer)[] {
|
||||
const arrLen = this._buff.readUInt8(this._offset, true); this._offset += 1;
|
||||
let arr: (string | Buffer)[] = new Array(arrLen);
|
||||
for (let i = 0; i < arrLen; i++) {
|
||||
const argType = <ArgType>this.readUInt8();
|
||||
if (argType === ArgType.String) {
|
||||
arr[i] = this.readLongString();
|
||||
} else {
|
||||
arr[i] = this.readBuffer();
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
class MessageFactory {
|
||||
public static cancel(req: string): string {
|
||||
return `{"type":${MessageType.Cancel},"id":"${req}"}`;
|
||||
class MessageIO {
|
||||
|
||||
private static _arrayContainsBuffer(arr: any[]): boolean {
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
if (Buffer.isBuffer(arr[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static request(req: string, rpcId: string, method: string, args: any[]): string {
|
||||
return `{"type":${MessageType.Request},"id":"${req}","proxyId":"${rpcId}","method":"${method}","args":${JSON.stringify(args)}}`;
|
||||
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 argsLengths: number[] = new Array(args.length);
|
||||
for (let i = 0, len = args.length; i < len; i++) {
|
||||
const arg = args[i];
|
||||
if (Buffer.isBuffer(arg)) {
|
||||
massagedArgs[i] = arg;
|
||||
argsLengths[i] = arg.byteLength;
|
||||
} else {
|
||||
massagedArgs[i] = safeStringify(arg, replacer);
|
||||
argsLengths[i] = Buffer.byteLength(massagedArgs[i], 'utf8');
|
||||
}
|
||||
}
|
||||
return this._requestMixedArgs(req, rpcId, method, massagedArgs, argsLengths, usesCancellationToken);
|
||||
}
|
||||
return this._requestJSONArgs(req, rpcId, method, safeStringify(args, replacer), usesCancellationToken);
|
||||
}
|
||||
|
||||
public static replyOK(req: string, res: any): string {
|
||||
private static _requestJSONArgs(req: number, rpcId: number, method: string, args: string, usesCancellationToken: boolean): Buffer {
|
||||
const methodByteLength = Buffer.byteLength(method, 'utf8');
|
||||
const argsByteLength = Buffer.byteLength(args, 'utf8');
|
||||
|
||||
let len = 0;
|
||||
len += MessageBuffer.sizeUInt8();
|
||||
len += MessageBuffer.sizeShortString(method, methodByteLength);
|
||||
len += MessageBuffer.sizeLongString(args, argsByteLength);
|
||||
|
||||
let result = MessageBuffer.alloc(usesCancellationToken ? MessageType.RequestJSONArgsWithCancellation : MessageType.RequestJSONArgs, req, len);
|
||||
result.writeUInt8(rpcId);
|
||||
result.writeShortString(method, methodByteLength);
|
||||
result.writeLongString(args, argsByteLength);
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
public static deserializeRequestJSONArgs(buff: MessageBuffer): { rpcId: number; method: string; args: any[]; } {
|
||||
const rpcId = buff.readUInt8();
|
||||
const method = buff.readShortString();
|
||||
const args = buff.readLongString();
|
||||
return {
|
||||
rpcId: rpcId,
|
||||
method: method,
|
||||
args: JSON.parse(args)
|
||||
};
|
||||
}
|
||||
|
||||
private static _requestMixedArgs(req: number, rpcId: number, method: string, args: (string | Buffer)[], argsLengths: number[], usesCancellationToken: boolean): Buffer {
|
||||
const methodByteLength = Buffer.byteLength(method, 'utf8');
|
||||
|
||||
let len = 0;
|
||||
len += MessageBuffer.sizeUInt8();
|
||||
len += MessageBuffer.sizeShortString(method, methodByteLength);
|
||||
len += MessageBuffer.sizeMixedArray(args, argsLengths);
|
||||
|
||||
let result = MessageBuffer.alloc(usesCancellationToken ? MessageType.RequestMixedArgsWithCancellation : MessageType.RequestMixedArgs, req, len);
|
||||
result.writeUInt8(rpcId);
|
||||
result.writeShortString(method, methodByteLength);
|
||||
result.writeMixedArray(args, argsLengths);
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
public static deserializeRequestMixedArgs(buff: MessageBuffer): { rpcId: number; method: string; args: any[]; } {
|
||||
const rpcId = buff.readUInt8();
|
||||
const method = buff.readShortString();
|
||||
const rawargs = buff.readMixedArray();
|
||||
const args: any[] = new Array(rawargs.length);
|
||||
for (let i = 0, len = rawargs.length; i < len; i++) {
|
||||
const rawarg = rawargs[i];
|
||||
if (typeof rawarg === 'string') {
|
||||
args[i] = JSON.parse(rawarg);
|
||||
} else {
|
||||
args[i] = rawarg;
|
||||
}
|
||||
}
|
||||
return {
|
||||
rpcId: rpcId,
|
||||
method: method,
|
||||
args: args
|
||||
};
|
||||
}
|
||||
|
||||
public static serializeAcknowledged(req: number): Buffer {
|
||||
return MessageBuffer.alloc(MessageType.Acknowledged, req, 0).buffer;
|
||||
}
|
||||
|
||||
public static serializeCancel(req: number): Buffer {
|
||||
return MessageBuffer.alloc(MessageType.Cancel, req, 0).buffer;
|
||||
}
|
||||
|
||||
public static serializeReplyOK(req: number, res: any, replacer: JSONStringifyReplacer | null): Buffer {
|
||||
if (typeof res === 'undefined') {
|
||||
return `{"type":${MessageType.Reply},"id":"${req}"}`;
|
||||
return this._serializeReplyOKEmpty(req);
|
||||
}
|
||||
return `{"type":${MessageType.Reply},"id":"${req}","res":${JSON.stringify(res)}}`;
|
||||
if (Buffer.isBuffer(res)) {
|
||||
return this._serializeReplyOKBuffer(req, res);
|
||||
}
|
||||
return this._serializeReplyOKJSON(req, safeStringify(res, replacer));
|
||||
}
|
||||
|
||||
public static replyErr(req: string, err: any): string {
|
||||
private static _serializeReplyOKEmpty(req: number): Buffer {
|
||||
return MessageBuffer.alloc(MessageType.ReplyOKEmpty, req, 0).buffer;
|
||||
}
|
||||
|
||||
private static _serializeReplyOKBuffer(req: number, res: Buffer): Buffer {
|
||||
const resByteLength = res.byteLength;
|
||||
|
||||
let len = 0;
|
||||
len += MessageBuffer.sizeBuffer(res, resByteLength);
|
||||
|
||||
let result = MessageBuffer.alloc(MessageType.ReplyOKBuffer, req, len);
|
||||
result.writeBuffer(res, resByteLength);
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
public static deserializeReplyOKBuffer(buff: MessageBuffer): Buffer {
|
||||
return buff.readBuffer();
|
||||
}
|
||||
|
||||
private static _serializeReplyOKJSON(req: number, res: string): Buffer {
|
||||
const resByteLength = Buffer.byteLength(res, 'utf8');
|
||||
|
||||
let len = 0;
|
||||
len += MessageBuffer.sizeLongString(res, resByteLength);
|
||||
|
||||
let result = MessageBuffer.alloc(MessageType.ReplyOKJSON, req, len);
|
||||
result.writeLongString(res, resByteLength);
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
public static deserializeReplyOKJSON(buff: MessageBuffer): any {
|
||||
const res = buff.readLongString();
|
||||
return JSON.parse(res);
|
||||
}
|
||||
|
||||
public static serializeReplyErr(req: number, err: any): Buffer {
|
||||
if (err instanceof Error) {
|
||||
return `{"type":${MessageType.ReplyErr},"id":"${req}","err":${JSON.stringify(errors.transformErrorForSerialization(err))}}`;
|
||||
return this._serializeReplyErrEror(req, err);
|
||||
}
|
||||
return `{"type":${MessageType.ReplyErr},"id":"${req}","err":null}`;
|
||||
return this._serializeReplyErrEmpty(req);
|
||||
}
|
||||
|
||||
private static _serializeReplyErrEror(req: number, _err: Error): Buffer {
|
||||
const err = safeStringify(errors.transformErrorForSerialization(_err), null);
|
||||
const errByteLength = Buffer.byteLength(err, 'utf8');
|
||||
|
||||
let len = 0;
|
||||
len += MessageBuffer.sizeLongString(err, errByteLength);
|
||||
|
||||
let result = MessageBuffer.alloc(MessageType.ReplyErrError, req, len);
|
||||
result.writeLongString(err, errByteLength);
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
public static deserializeReplyErrError(buff: MessageBuffer): Error {
|
||||
const err = buff.readLongString();
|
||||
return JSON.parse(err);
|
||||
}
|
||||
|
||||
private static _serializeReplyErrEmpty(req: number): Buffer {
|
||||
return MessageBuffer.alloc(MessageType.ReplyErrEmpty, req, 0).buffer;
|
||||
}
|
||||
}
|
||||
|
||||
const enum MessageType {
|
||||
Request = 1,
|
||||
Cancel = 2,
|
||||
Reply = 3,
|
||||
ReplyErr = 4
|
||||
RequestJSONArgs = 1,
|
||||
RequestJSONArgsWithCancellation = 2,
|
||||
RequestMixedArgs = 3,
|
||||
RequestMixedArgsWithCancellation = 4,
|
||||
Acknowledged = 5,
|
||||
Cancel = 6,
|
||||
ReplyOKEmpty = 7,
|
||||
ReplyOKBuffer = 8,
|
||||
ReplyOKJSON = 9,
|
||||
ReplyErrError = 10,
|
||||
ReplyErrEmpty = 11,
|
||||
}
|
||||
|
||||
class RequestMessage {
|
||||
type: MessageType.Request;
|
||||
id: string;
|
||||
proxyId: string;
|
||||
method: string;
|
||||
args: any[];
|
||||
const enum ArgType {
|
||||
String = 1,
|
||||
Buffer = 2
|
||||
}
|
||||
class CancelMessage {
|
||||
type: MessageType.Cancel;
|
||||
id: string;
|
||||
}
|
||||
class ReplyMessage {
|
||||
type: MessageType.Reply;
|
||||
id: string;
|
||||
res: any;
|
||||
}
|
||||
class ReplyErrMessage {
|
||||
type: MessageType.ReplyErr;
|
||||
id: string;
|
||||
err: errors.SerializedError;
|
||||
}
|
||||
|
||||
type RPCMessage = RequestMessage | CancelMessage | ReplyMessage | ReplyErrMessage;
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier';
|
||||
import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol';
|
||||
|
||||
suite('RPCProtocol', () => {
|
||||
|
||||
class MessagePassingProtocol implements IMessagePassingProtocol {
|
||||
private _pair: MessagePassingProtocol;
|
||||
|
||||
private readonly _onMessage: Emitter<Buffer> = new Emitter<Buffer>();
|
||||
public readonly onMessage: Event<Buffer> = this._onMessage.event;
|
||||
|
||||
public setPair(other: MessagePassingProtocol) {
|
||||
this._pair = other;
|
||||
}
|
||||
|
||||
public send(buffer: Buffer): void {
|
||||
process.nextTick(() => {
|
||||
this._pair._onMessage.fire(buffer);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let delegate: (a1: any, a2: any) => any;
|
||||
let bProxy: BClass;
|
||||
class BClass {
|
||||
$m(a1: any, a2: any): Thenable<any> {
|
||||
return Promise.resolve(delegate.call(null, a1, a2));
|
||||
}
|
||||
}
|
||||
|
||||
setup(() => {
|
||||
let a_protocol = new MessagePassingProtocol();
|
||||
let b_protocol = new MessagePassingProtocol();
|
||||
a_protocol.setPair(b_protocol);
|
||||
b_protocol.setPair(a_protocol);
|
||||
|
||||
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);
|
||||
bProxy = A.getProxy(bIdentifier);
|
||||
});
|
||||
|
||||
test('simple call', function (done) {
|
||||
delegate = (a1: number, a2: number) => a1 + a2;
|
||||
bProxy.$m(4, 1).then((res: number) => {
|
||||
assert.equal(res, 5);
|
||||
done(null);
|
||||
}, done);
|
||||
});
|
||||
|
||||
test('simple call without result', function (done) {
|
||||
delegate = (a1: number, a2: number) => { };
|
||||
bProxy.$m(4, 1).then((res: number) => {
|
||||
assert.equal(res, undefined);
|
||||
done(null);
|
||||
}, done);
|
||||
});
|
||||
|
||||
test('passing buffer as argument', function (done) {
|
||||
delegate = (a1: Buffer, a2: number) => {
|
||||
assert.ok(Buffer.isBuffer(a1));
|
||||
return a1[a2];
|
||||
};
|
||||
let b = Buffer.allocUnsafe(4);
|
||||
b[0] = 1;
|
||||
b[1] = 2;
|
||||
b[2] = 3;
|
||||
b[3] = 4;
|
||||
bProxy.$m(b, 2).then((res: number) => {
|
||||
assert.equal(res, 3);
|
||||
done(null);
|
||||
}, done);
|
||||
});
|
||||
|
||||
test('returning a buffer', function (done) {
|
||||
delegate = (a1: number, a2: number) => {
|
||||
let b = Buffer.allocUnsafe(4);
|
||||
b[0] = 1;
|
||||
b[1] = 2;
|
||||
b[2] = 3;
|
||||
b[3] = 4;
|
||||
return b;
|
||||
};
|
||||
bProxy.$m(4, 1).then((res: Buffer) => {
|
||||
assert.ok(Buffer.isBuffer(res));
|
||||
assert.equal(res[0], 1);
|
||||
assert.equal(res[1], 2);
|
||||
assert.equal(res[2], 3);
|
||||
assert.equal(res[3], 4);
|
||||
done(null);
|
||||
}, done);
|
||||
});
|
||||
|
||||
test('cancelling a call via CancellationToken before', function (done) {
|
||||
delegate = (a1: number, a2: number) => a1 + a2;
|
||||
let p = bProxy.$m(4, CancellationToken.Cancelled);
|
||||
p.then((res: number) => {
|
||||
assert.fail('should not receive result');
|
||||
}, (err) => {
|
||||
assert.ok(true);
|
||||
done(null);
|
||||
});
|
||||
});
|
||||
|
||||
test('passing CancellationToken.None', function (done) {
|
||||
delegate = (a1: number, token: CancellationToken) => {
|
||||
assert.ok(!!token);
|
||||
return a1 + 1;
|
||||
};
|
||||
bProxy.$m(4, CancellationToken.None).then((res: number) => {
|
||||
assert.equal(res, 5);
|
||||
done(null);
|
||||
}, done);
|
||||
});
|
||||
|
||||
test('cancelling a call via CancellationToken quickly', function (done) {
|
||||
// this is an implementation which, when cancellation is triggered, will return 7
|
||||
delegate = (a1: number, token: CancellationToken) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
token.onCancellationRequested((e) => {
|
||||
resolve(7);
|
||||
});
|
||||
});
|
||||
};
|
||||
let tokenSource = new CancellationTokenSource();
|
||||
let p = bProxy.$m(4, tokenSource.token);
|
||||
p.then((res: number) => {
|
||||
assert.equal(res, 7);
|
||||
done(null);
|
||||
}, (err) => {
|
||||
assert.fail('should not receive error');
|
||||
done();
|
||||
});
|
||||
tokenSource.cancel();
|
||||
});
|
||||
|
||||
test('throwing an error', function (done) {
|
||||
delegate = (a1: number, a2: number) => {
|
||||
throw new Error(`nope`);
|
||||
};
|
||||
bProxy.$m(4, 1).then((res) => {
|
||||
assert.fail('unexpected');
|
||||
done(null);
|
||||
}, (err) => {
|
||||
assert.equal(err.message, 'nope');
|
||||
done(null);
|
||||
});
|
||||
});
|
||||
|
||||
test('error promise', function (done) {
|
||||
delegate = (a1: number, a2: number) => {
|
||||
return Promise.reject(undefined);
|
||||
};
|
||||
bProxy.$m(4, 1).then((res) => {
|
||||
assert.fail('unexpected');
|
||||
done(null);
|
||||
}, (err) => {
|
||||
assert.equal(err, undefined);
|
||||
done(null);
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #60450: Converting circular structure to JSON', function (done) {
|
||||
delegate = (a1: number, a2: number) => {
|
||||
let circular = <any>{};
|
||||
circular.self = circular;
|
||||
return circular;
|
||||
};
|
||||
bProxy.$m(4, 1).then((res) => {
|
||||
assert.equal(res, null);
|
||||
done(null);
|
||||
}, (err) => {
|
||||
assert.fail('unexpected');
|
||||
done(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user