mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-14 18:46:34 -05:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
@@ -0,0 +1,500 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { stringify } from 'vs/base/common/marshalling';
|
||||
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 { IMessageService, Severity } from 'vs/platform/message/common/message';
|
||||
import { ILifecycleService, ShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { IWorkspaceContextService } 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 { 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, any } from 'vs/base/common/event';
|
||||
import { fromEventEmitter } from 'vs/base/node/event';
|
||||
import { IInitData, IWorkspaceData } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
|
||||
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { ICrashReporterService } from 'vs/workbench/services/crashReporter/common/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, ILogEntry, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class ExtensionHostProcessWorker {
|
||||
|
||||
private _onCrashed: Emitter<[number, string]> = new Emitter<[number, string]>();
|
||||
public readonly onCrashed: Event<[number, string]> = this._onCrashed.event;
|
||||
|
||||
private readonly _toDispose: IDisposable[];
|
||||
|
||||
private readonly _isExtensionDevHost: boolean;
|
||||
private readonly _isExtensionDevDebug: boolean;
|
||||
private readonly _isExtensionDevDebugBrk: boolean;
|
||||
private readonly _isExtensionDevTestFromCli: boolean;
|
||||
|
||||
// State
|
||||
private _lastExtensionHostError: string;
|
||||
private _terminating: boolean;
|
||||
|
||||
// Resources, in order they get acquired/created when .start() is called:
|
||||
private _namedPipeServer: Server;
|
||||
private _extensionHostProcess: ChildProcess;
|
||||
private _extensionHostConnection: Socket;
|
||||
private _messageProtocol: TPromise<IMessagePassingProtocol>;
|
||||
|
||||
constructor(
|
||||
/* intentionally not injected */private readonly _extensionService: IExtensionService,
|
||||
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
|
||||
@IMessageService private readonly _messageService: IMessageService,
|
||||
@IWindowsService private readonly _windowsService: IWindowsService,
|
||||
@IWindowService private readonly _windowService: IWindowService,
|
||||
@IBroadcastService private readonly _broadcastService: IBroadcastService,
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
@IWorkspaceConfigurationService private readonly _configurationService: IWorkspaceConfigurationService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@ICrashReporterService private readonly _crashReporterService: ICrashReporterService
|
||||
) {
|
||||
// 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;
|
||||
|
||||
this._lastExtensionHostError = null;
|
||||
this._terminating = false;
|
||||
|
||||
this._namedPipeServer = null;
|
||||
this._extensionHostProcess = null;
|
||||
this._extensionHostConnection = null;
|
||||
this._messageProtocol = null;
|
||||
|
||||
this._toDispose = [];
|
||||
this._toDispose.push(this._onCrashed);
|
||||
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)));
|
||||
|
||||
const globalExitListener = () => this.terminate();
|
||||
process.once('exit', globalExitListener);
|
||||
this._toDispose.push({
|
||||
dispose: () => {
|
||||
process.removeListener('exit', globalExitListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.terminate();
|
||||
}
|
||||
|
||||
private _onBroadcast(broadcast: IBroadcast): void {
|
||||
|
||||
// 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))) {
|
||||
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))) {
|
||||
this._windowService.reloadWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public start(): TPromise<IMessagePassingProtocol> {
|
||||
if (this._terminating) {
|
||||
// .terminate() was called
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this._messageProtocol) {
|
||||
this._messageProtocol = TPromise.join<any>([this._tryListenOnPipe(), this._tryFindDebugPort()]).then((data: [string, number]) => {
|
||||
const pipeName = data[0];
|
||||
// The port will be 0 if there's no need to debug or if a free port was not found
|
||||
const port = data[1];
|
||||
|
||||
const opts = {
|
||||
env: objects.mixin(objects.clone(process.env), {
|
||||
AMD_ENTRYPOINT: 'vs/workbench/node/extensionHostProcess',
|
||||
PIPE_LOGGING: 'true',
|
||||
VERBOSE_LOGGING: true,
|
||||
VSCODE_WINDOW_ID: String(this._windowService.getCurrentWindowId()),
|
||||
VSCODE_IPC_HOOK_EXTHOST: pipeName,
|
||||
VSCODE_HANDLES_UNCAUGHT_ERRORS: true,
|
||||
ELECTRON_NO_ASAR: '1'
|
||||
}),
|
||||
// We only detach the extension host on windows. Linux and Mac orphan by default
|
||||
// and detach under Linux and Mac create another process group.
|
||||
// We detach because we have noticed that when the renderer exits, its child processes
|
||||
// (i.e. extension host) are taken down in a brutal fashion by the OS
|
||||
detached: !!isWindows,
|
||||
execArgv: port
|
||||
? ['--nolazy', (this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + port]
|
||||
: undefined,
|
||||
silent: true
|
||||
};
|
||||
|
||||
const crashReporterOptions = this._crashReporterService.getChildProcessStartOptions('extensionHost');
|
||||
if (crashReporterOptions) {
|
||||
opts.env.CRASH_REPORTER_START_OPTIONS = JSON.stringify(crashReporterOptions);
|
||||
}
|
||||
|
||||
// Run Extension Host as fork of current process
|
||||
this._extensionHostProcess = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts);
|
||||
|
||||
// Catch all output coming from the extension host process
|
||||
type Output = { data: string, format: string[] };
|
||||
this._extensionHostProcess.stdout.setEncoding('utf8');
|
||||
this._extensionHostProcess.stderr.setEncoding('utf8');
|
||||
const onStdout = fromEventEmitter<string>(this._extensionHostProcess.stdout, 'data');
|
||||
const onStderr = fromEventEmitter<string>(this._extensionHostProcess.stderr, 'data');
|
||||
const onOutput = any(
|
||||
mapEvent(onStdout, o => ({ data: `%c${o}`, format: [''] })),
|
||||
mapEvent(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }))
|
||||
);
|
||||
|
||||
// Debounce all output, so we can render it in the Chrome console as a group
|
||||
const onDebouncedOutput = debounceEvent<Output>(onOutput, (r, o) => {
|
||||
return r
|
||||
? { data: r.data + o.data, format: [...r.format, ...o.format] }
|
||||
: { data: o.data, format: o.format };
|
||||
}, 100);
|
||||
|
||||
// Print out extension host output
|
||||
onDebouncedOutput(data => {
|
||||
console.group('Extension Host');
|
||||
console.log(data.data, ...data.format);
|
||||
console.groupEnd();
|
||||
});
|
||||
|
||||
// Support logging from extension host
|
||||
this._extensionHostProcess.on('message', msg => {
|
||||
if (msg && (<ILogEntry>msg).type === '__$console') {
|
||||
this._logExtensionHostMessage(<ILogEntry>msg);
|
||||
}
|
||||
});
|
||||
|
||||
// Lifecycle
|
||||
this._extensionHostProcess.on('error', (err) => this._onExtHostProcessError(err));
|
||||
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 && port) {
|
||||
this._broadcastService.broadcast({
|
||||
channel: EXTENSION_ATTACH_BROADCAST_CHANNEL,
|
||||
payload: {
|
||||
debugId: this._environmentService.debugExtensionHost.debugId,
|
||||
port
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Help in case we fail to start it
|
||||
let startupTimeoutHandle: number;
|
||||
if (!this._environmentService.isBuilt || 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.")
|
||||
: nls.localize('extensionHostProcess.startupFail', "Extension host did not start in 10 seconds, that might be a problem.");
|
||||
|
||||
this._messageService.show(Severity.Warning, msg);
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// Initialize extension host process with hand shakes
|
||||
return this._tryExtHostHandshake().then((protocol) => {
|
||||
clearTimeout(startupTimeoutHandle);
|
||||
return protocol;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return this._messageProtocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
const pipeName = generateRandomPipeName();
|
||||
|
||||
this._namedPipeServer = createServer();
|
||||
this._namedPipeServer.on('error', reject);
|
||||
this._namedPipeServer.listen(pipeName, () => {
|
||||
this._namedPipeServer.removeListener('error', reject);
|
||||
resolve(pipeName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a free port if extension host debugging is enabled.
|
||||
*/
|
||||
private _tryFindDebugPort(): TPromise<number> {
|
||||
const extensionHostPort = this._environmentService.debugExtensionHost.port;
|
||||
if (typeof extensionHostPort !== 'number') {
|
||||
return TPromise.wrap<number>(0);
|
||||
}
|
||||
return new TPromise<number>((c, e) => {
|
||||
findFreePort(extensionHostPort, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, (port) => {
|
||||
if (!port) {
|
||||
console.warn('%c[Extension Host] %cCould not find a free port for debugging', 'color: blue', 'color: black');
|
||||
return c(void 0);
|
||||
}
|
||||
if (port !== extensionHostPort) {
|
||||
console.warn(`%c[Extension Host] %cProvided debugging port ${extensionHostPort} is not free, using ${port} instead.`, 'color: blue', 'color: black');
|
||||
}
|
||||
if (this._isExtensionDevDebugBrk) {
|
||||
console.warn(`%c[Extension Host] %cSTOPPED on first line for debugging on port ${port}`, 'color: blue', 'color: black');
|
||||
} else {
|
||||
console.info(`%c[Extension Host] %cdebugger listening on port ${port}`, 'color: blue', 'color: black');
|
||||
}
|
||||
return c(port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _tryExtHostHandshake(): TPromise<IMessagePassingProtocol> {
|
||||
|
||||
return new TPromise<IMessagePassingProtocol>((resolve, reject) => {
|
||||
|
||||
// Wait for the extension host to connect to our named pipe
|
||||
// and wrap the socket in the message passing protocol
|
||||
let handle = setTimeout(() => {
|
||||
this._namedPipeServer.close();
|
||||
this._namedPipeServer = null;
|
||||
reject('timeout');
|
||||
}, 60 * 1000);
|
||||
|
||||
this._namedPipeServer.on('connection', socket => {
|
||||
clearTimeout(handle);
|
||||
this._namedPipeServer.close();
|
||||
this._namedPipeServer = null;
|
||||
this._extensionHostConnection = socket;
|
||||
resolve(new Protocol(this._extensionHostConnection));
|
||||
});
|
||||
|
||||
}).then((protocol) => {
|
||||
|
||||
// 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) => {
|
||||
|
||||
let handle = setTimeout(() => {
|
||||
reject('timeout');
|
||||
}, 60 * 1000);
|
||||
|
||||
const disposable = protocol.onMessage(msg => {
|
||||
|
||||
if (msg === 'ready') {
|
||||
// 1) Extension Host is ready to receive messages, initialize it
|
||||
this._createExtHostInitData().then(data => protocol.send(stringify(data)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg === 'initialized') {
|
||||
// 2) Extension Host is initialized
|
||||
|
||||
clearTimeout(handle);
|
||||
|
||||
// stop listening for messages here
|
||||
disposable.dispose();
|
||||
|
||||
// release this promise
|
||||
resolve(protocol);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`received unexpected message during handshake phase from the extension host: `, msg);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private _createExtHostInitData(): TPromise<IInitData> {
|
||||
return TPromise.join<any>([this._telemetryService.getTelemetryInfo(), this._extensionService.getExtensions()]).then(([telemetryInfo, extensionDescriptions]) => {
|
||||
const r: IInitData = {
|
||||
parentPid: process.pid,
|
||||
environment: {
|
||||
isExtensionDevelopmentDebug: this._isExtensionDevDebug,
|
||||
appRoot: this._environmentService.appRoot,
|
||||
appSettingsHome: this._environmentService.appSettingsHome,
|
||||
disableExtensions: this._environmentService.disableExtensions,
|
||||
userExtensionsHome: this._environmentService.extensionsPath,
|
||||
extensionDevelopmentPath: this._environmentService.extensionDevelopmentPath,
|
||||
extensionTestsPath: this._environmentService.extensionTestsPath,
|
||||
// globally disable proposed api when built and not insiders developing extensions
|
||||
enableProposedApiForAll: !this._environmentService.isBuilt || (!!this._environmentService.extensionDevelopmentPath && product.nameLong.indexOf('Insiders') >= 0),
|
||||
enableProposedApiFor: this._environmentService.args['enable-proposed-api'] || []
|
||||
},
|
||||
workspace: <IWorkspaceData>this._contextService.getWorkspace(),
|
||||
extensions: extensionDescriptions,
|
||||
configuration: this._configurationService.getConfigurationData(),
|
||||
telemetryInfo
|
||||
};
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
private _logExtensionHostMessage(logEntry: ILogEntry) {
|
||||
let args = [];
|
||||
try {
|
||||
let parsed = JSON.parse(logEntry.arguments);
|
||||
args.push(...Object.getOwnPropertyNames(parsed).map(o => parsed[o]));
|
||||
} catch (error) {
|
||||
args.push(logEntry.arguments);
|
||||
}
|
||||
|
||||
// If the first argument is a string, check for % which indicates that the message
|
||||
// uses substitution for variables. In this case, we cannot just inject our colored
|
||||
// [Extension Host] to the front because it breaks substitution.
|
||||
let consoleArgs = [];
|
||||
if (typeof args[0] === 'string' && args[0].indexOf('%') >= 0) {
|
||||
consoleArgs = [`%c[Extension Host]%c ${args[0]}`, 'color: blue', 'color: black', ...args.slice(1)];
|
||||
} else {
|
||||
consoleArgs = ['%c[Extension Host]', 'color: blue', ...args];
|
||||
}
|
||||
|
||||
// Send to local console unless we run tests from cli
|
||||
if (!this._isExtensionDevTestFromCli) {
|
||||
console[logEntry.severity].apply(console, consoleArgs);
|
||||
}
|
||||
|
||||
// Log on main side if running tests from cli
|
||||
if (this._isExtensionDevTestFromCli) {
|
||||
this._windowsService.log(logEntry.severity, ...args);
|
||||
}
|
||||
|
||||
// Broadcast to other windows if we are in development mode
|
||||
else if (!this._environmentService.isBuilt || this._isExtensionDevHost) {
|
||||
this._broadcastService.broadcast({
|
||||
channel: EXTENSION_LOG_BROADCAST_CHANNEL,
|
||||
payload: {
|
||||
logEntry,
|
||||
debugId: this._environmentService.debugExtensionHost.debugId
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _onExtHostProcessError(err: any): void {
|
||||
let errorMessage = toErrorMessage(err);
|
||||
if (errorMessage === this._lastExtensionHostError) {
|
||||
return; // prevent error spam
|
||||
}
|
||||
|
||||
this._lastExtensionHostError = errorMessage;
|
||||
|
||||
this._messageService.show(Severity.Error, nls.localize('extensionHostProcess.error', "Error from the extension host: {0}", errorMessage));
|
||||
}
|
||||
|
||||
private _onExtHostProcessExit(code: number, signal: string): void {
|
||||
if (this._terminating) {
|
||||
// Expected termination path (we asked the process to terminate)
|
||||
return;
|
||||
}
|
||||
|
||||
// Unexpected termination
|
||||
if (!this._isExtensionDevHost) {
|
||||
this._onCrashed.fire([code, signal]);
|
||||
}
|
||||
|
||||
// Expected development extension termination: When the extension host goes down we also shutdown the window
|
||||
else if (!this._isExtensionDevTestFromCli) {
|
||||
this._windowService.closeWindow();
|
||||
}
|
||||
|
||||
// When CLI testing make sure to exit with proper exit code
|
||||
else {
|
||||
ipc.send('vscode:exit', code);
|
||||
}
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._terminating) {
|
||||
return;
|
||||
}
|
||||
this._terminating = true;
|
||||
|
||||
dispose(this._toDispose);
|
||||
|
||||
if (!this._messageProtocol) {
|
||||
// .start() was not called
|
||||
return;
|
||||
}
|
||||
|
||||
this._messageProtocol.then((protocol) => {
|
||||
|
||||
// Send the extension host a request to terminate itself
|
||||
// (graceful termination)
|
||||
protocol.send({
|
||||
type: '__$terminate'
|
||||
});
|
||||
|
||||
// Give the extension host 60s, after which we will
|
||||
// try to kill the process and release any resources
|
||||
setTimeout(() => this._cleanResources(), 60 * 1000);
|
||||
|
||||
}, (err) => {
|
||||
|
||||
// Establishing a protocol with the extension host failed, so
|
||||
// try to kill the process and release any resources.
|
||||
this._cleanResources();
|
||||
});
|
||||
}
|
||||
|
||||
private _cleanResources(): void {
|
||||
if (this._namedPipeServer) {
|
||||
this._namedPipeServer.close();
|
||||
this._namedPipeServer = null;
|
||||
}
|
||||
if (this._extensionHostConnection) {
|
||||
this._extensionHostConnection.end();
|
||||
this._extensionHostConnection = null;
|
||||
}
|
||||
if (this._extensionHostProcess) {
|
||||
this._extensionHostProcess.kill();
|
||||
this._extensionHostProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _onWillShutdown(event: ShutdownEvent): 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
|
||||
if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug) {
|
||||
this._broadcastService.broadcast({
|
||||
channel: EXTENSION_TERMINATE_BROADCAST_CHANNEL,
|
||||
payload: {
|
||||
debugId: this._environmentService.debugExtensionHost.debugId
|
||||
}
|
||||
});
|
||||
|
||||
event.veto(TPromise.timeout(100 /* wait a bit for IPC to get delivered */).then(() => false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 Platform from 'vs/base/common/platform';
|
||||
import pfs = require('vs/base/node/pfs');
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { groupBy, values } from 'vs/base/common/collections';
|
||||
import { join, normalize, extname } from 'path';
|
||||
import json = require('vs/base/common/json');
|
||||
import Types = require('vs/base/common/types');
|
||||
import { isValidExtensionDescription } from 'vs/platform/extensions/node/extensionValidator';
|
||||
import * as semver from 'semver';
|
||||
import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
|
||||
|
||||
const MANIFEST_FILE = 'package.json';
|
||||
|
||||
const devMode = !!process.env['VSCODE_DEV'];
|
||||
interface NlsConfiguration {
|
||||
locale: string;
|
||||
pseudo: boolean;
|
||||
}
|
||||
const nlsConfig: NlsConfiguration = {
|
||||
locale: Platform.locale,
|
||||
pseudo: Platform.locale === 'pseudo'
|
||||
};
|
||||
|
||||
export interface ILog {
|
||||
error(source: string, message: string): void;
|
||||
warn(source: string, message: string): void;
|
||||
info(source: string, message: string): void;
|
||||
}
|
||||
|
||||
abstract class ExtensionManifestHandler {
|
||||
|
||||
protected _ourVersion: string;
|
||||
protected _log: ILog;
|
||||
protected _absoluteFolderPath: string;
|
||||
protected _isBuiltin: boolean;
|
||||
protected _absoluteManifestPath: string;
|
||||
|
||||
constructor(ourVersion: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean) {
|
||||
this._ourVersion = ourVersion;
|
||||
this._log = log;
|
||||
this._absoluteFolderPath = absoluteFolderPath;
|
||||
this._isBuiltin = isBuiltin;
|
||||
this._absoluteManifestPath = join(absoluteFolderPath, MANIFEST_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionManifestParser extends ExtensionManifestHandler {
|
||||
|
||||
public parse(): TPromise<IExtensionDescription> {
|
||||
return pfs.readFile(this._absoluteManifestPath).then((manifestContents) => {
|
||||
try {
|
||||
return JSON.parse(manifestContents.toString());
|
||||
} catch (e) {
|
||||
this._log.error(this._absoluteFolderPath, nls.localize('jsonParseFail', "Failed to parse {0}: {1}.", this._absoluteManifestPath, getParseErrorMessage(e.message)));
|
||||
}
|
||||
return null;
|
||||
}, (err) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
|
||||
this._log.error(this._absoluteFolderPath, nls.localize('fileReadFail', "Cannot read file {0}: {1}.", this._absoluteManifestPath, err.message));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
|
||||
|
||||
public replaceNLS(extensionDescription: IExtensionDescription): TPromise<IExtensionDescription> {
|
||||
let extension = extname(this._absoluteManifestPath);
|
||||
let basename = this._absoluteManifestPath.substr(0, this._absoluteManifestPath.length - extension.length);
|
||||
|
||||
return pfs.fileExists(basename + '.nls' + extension).then(exists => {
|
||||
if (!exists) {
|
||||
return extensionDescription;
|
||||
}
|
||||
return ExtensionManifestNLSReplacer.findMessageBundles(basename).then((messageBundle) => {
|
||||
if (!messageBundle.localized) {
|
||||
return extensionDescription;
|
||||
}
|
||||
return pfs.readFile(messageBundle.localized).then(messageBundleContent => {
|
||||
let errors: json.ParseError[] = [];
|
||||
let messages: { [key: string]: string; } = json.parse(messageBundleContent.toString(), errors);
|
||||
|
||||
return ExtensionManifestNLSReplacer.resolveOriginalMessageBundle(messageBundle.original, errors).then(originalMessages => {
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error) => {
|
||||
this._log.error(this._absoluteFolderPath, nls.localize('jsonsParseFail', "Failed to parse {0} or {1}: {2}.", messageBundle.localized, messageBundle.original, getParseErrorMessage(error.error)));
|
||||
});
|
||||
return extensionDescription;
|
||||
}
|
||||
|
||||
ExtensionManifestNLSReplacer._replaceNLStrings(extensionDescription, messages, originalMessages, this._log, this._absoluteFolderPath);
|
||||
return extensionDescription;
|
||||
});
|
||||
}, (err) => {
|
||||
this._log.error(this._absoluteFolderPath, nls.localize('fileReadFail', "Cannot read file {0}: {1}.", messageBundle.localized, err.message));
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, p) => {
|
||||
if (originalMessageBundle) {
|
||||
pfs.readFile(originalMessageBundle).then(originalBundleContent => {
|
||||
c(json.parse(originalBundleContent.toString(), errors));
|
||||
});
|
||||
} else {
|
||||
c(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(basename: string): TPromise<{ localized: string, original: string }> {
|
||||
return new TPromise<{ localized: string, original: string }>((c, e, p) => {
|
||||
function loop(basename: string, locale: string): void {
|
||||
let toCheck = `${basename}.nls.${locale}.json`;
|
||||
pfs.fileExists(toCheck).then(exists => {
|
||||
if (exists) {
|
||||
c({ localized: toCheck, original: `${basename}.nls.json` });
|
||||
}
|
||||
let index = locale.lastIndexOf('-');
|
||||
if (index === -1) {
|
||||
c({ localized: `${basename}.nls.json`, original: null });
|
||||
} else {
|
||||
locale = locale.substring(0, index);
|
||||
loop(basename, locale);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (devMode || nlsConfig.pseudo || !nlsConfig.locale) {
|
||||
return c({ localized: basename + '.nls.json', original: null });
|
||||
}
|
||||
loop(basename, nlsConfig.locale);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This routine makes the following assumptions:
|
||||
* The root element is an object literal
|
||||
*/
|
||||
private static _replaceNLStrings<T>(literal: T, messages: { [key: string]: string; }, originalMessages: { [key: string]: string }, log: ILog, messageScope: string): void {
|
||||
function processEntry(obj: any, key: string | number, command?: boolean) {
|
||||
let value = obj[key];
|
||||
if (Types.isString(value)) {
|
||||
let str = <string>value;
|
||||
let length = str.length;
|
||||
if (length > 1 && str[0] === '%' && str[length - 1] === '%') {
|
||||
let messageKey = str.substr(1, length - 2);
|
||||
let message = messages[messageKey];
|
||||
if (message) {
|
||||
if (nlsConfig.pseudo) {
|
||||
// FF3B and FF3D is the Unicode zenkaku representation for [ and ]
|
||||
message = '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D';
|
||||
}
|
||||
obj[key] = command && (key === 'title' || key === 'category') && originalMessages ? { value: message, original: originalMessages[messageKey] } : message;
|
||||
} else {
|
||||
log.warn(messageScope, nls.localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey));
|
||||
}
|
||||
}
|
||||
} else if (Types.isObject(value)) {
|
||||
for (let k in value) {
|
||||
if (value.hasOwnProperty(k)) {
|
||||
k === 'commands' ? processEntry(value, k, true) : processEntry(value, k, command);
|
||||
}
|
||||
}
|
||||
} else if (Types.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
processEntry(value, i, command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let key in literal) {
|
||||
if (literal.hasOwnProperty(key)) {
|
||||
processEntry(literal, key);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionManifestValidator extends ExtensionManifestHandler {
|
||||
validate(_extensionDescription: IExtensionDescription): IExtensionDescription {
|
||||
// Relax the readonly properties here, it is the one place where we check and normalize values
|
||||
interface IRelaxedExtensionDescription {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
publisher: string;
|
||||
isBuiltin: boolean;
|
||||
extensionFolderPath: string;
|
||||
engines: {
|
||||
vscode: string;
|
||||
};
|
||||
main?: string;
|
||||
enableProposedApi?: boolean;
|
||||
}
|
||||
let extensionDescription = <IRelaxedExtensionDescription>_extensionDescription;
|
||||
extensionDescription.isBuiltin = this._isBuiltin;
|
||||
|
||||
let notices: string[] = [];
|
||||
if (!isValidExtensionDescription(this._ourVersion, this._absoluteFolderPath, extensionDescription, notices)) {
|
||||
notices.forEach((error) => {
|
||||
this._log.error(this._absoluteFolderPath, error);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// in this case the notices are warnings
|
||||
notices.forEach((error) => {
|
||||
this._log.warn(this._absoluteFolderPath, error);
|
||||
});
|
||||
|
||||
// id := `publisher.name`
|
||||
extensionDescription.id = `${extensionDescription.publisher}.${extensionDescription.name}`;
|
||||
|
||||
// main := absolutePath(`main`)
|
||||
if (extensionDescription.main) {
|
||||
extensionDescription.main = join(this._absoluteFolderPath, extensionDescription.main);
|
||||
}
|
||||
|
||||
extensionDescription.extensionFolderPath = this._absoluteFolderPath;
|
||||
|
||||
return extensionDescription;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtensionScanner {
|
||||
|
||||
/**
|
||||
* Read the extension defined in `absoluteFolderPath`
|
||||
*/
|
||||
public static scanExtension(
|
||||
version: string,
|
||||
log: ILog,
|
||||
absoluteFolderPath: string,
|
||||
isBuiltin: boolean
|
||||
): TPromise<IExtensionDescription> {
|
||||
absoluteFolderPath = normalize(absoluteFolderPath);
|
||||
|
||||
let parser = new ExtensionManifestParser(version, log, absoluteFolderPath, isBuiltin);
|
||||
return parser.parse().then((extensionDescription) => {
|
||||
if (extensionDescription === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nlsReplacer = new ExtensionManifestNLSReplacer(version, log, absoluteFolderPath, isBuiltin);
|
||||
return nlsReplacer.replaceNLS(extensionDescription);
|
||||
}).then((extensionDescription) => {
|
||||
if (extensionDescription === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let validator = new ExtensionManifestValidator(version, log, absoluteFolderPath, isBuiltin);
|
||||
return validator.validate(extensionDescription);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a list of extensions defined in `absoluteFolderPath`
|
||||
*/
|
||||
public static scanExtensions(
|
||||
version: string,
|
||||
log: ILog,
|
||||
absoluteFolderPath: string,
|
||||
isBuiltin: boolean
|
||||
): TPromise<IExtensionDescription[]> {
|
||||
let obsolete = TPromise.as({});
|
||||
|
||||
if (!isBuiltin) {
|
||||
obsolete = pfs.readFile(join(absoluteFolderPath, '.obsolete'), 'utf8')
|
||||
.then(raw => JSON.parse(raw))
|
||||
.then(null, err => ({}));
|
||||
}
|
||||
|
||||
return obsolete.then(obsolete => {
|
||||
return pfs.readDirsInDir(absoluteFolderPath)
|
||||
.then(folders => {
|
||||
if (isBuiltin) {
|
||||
return folders;
|
||||
}
|
||||
|
||||
// TODO: align with extensionsService
|
||||
const nonGallery: string[] = [];
|
||||
const gallery: { folder: string; id: string; version: string; }[] = [];
|
||||
|
||||
folders.forEach(folder => {
|
||||
if (obsolete[folder]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, version } = getIdAndVersionFromLocalExtensionId(folder);
|
||||
|
||||
if (!id || !version) {
|
||||
nonGallery.push(folder);
|
||||
return;
|
||||
}
|
||||
|
||||
gallery.push({ folder, id, version });
|
||||
});
|
||||
|
||||
const byId = values(groupBy(gallery, p => p.id));
|
||||
const latest = byId.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0])
|
||||
.map(a => a.folder);
|
||||
|
||||
return [...nonGallery, ...latest];
|
||||
})
|
||||
.then(folders => TPromise.join(folders.map(f => this.scanExtension(version, log, join(absoluteFolderPath, f), isBuiltin))))
|
||||
.then(extensionDescriptions => extensionDescriptions.filter(item => item !== null))
|
||||
.then(null, err => {
|
||||
log.error(absoluteFolderPath, err);
|
||||
return [];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
version: string,
|
||||
log: ILog,
|
||||
absoluteFolderPath: string,
|
||||
isBuiltin: boolean
|
||||
): TPromise<IExtensionDescription[]> {
|
||||
return pfs.fileExists(join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => {
|
||||
if (exists) {
|
||||
return this.scanExtension(version, log, absoluteFolderPath, isBuiltin).then((extensionDescription) => {
|
||||
if (extensionDescription === null) {
|
||||
return [];
|
||||
}
|
||||
return [extensionDescription];
|
||||
});
|
||||
}
|
||||
return this.scanExtensions(version, log, absoluteFolderPath, isBuiltin);
|
||||
}, (err) => {
|
||||
log.error(absoluteFolderPath, err);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 Severity from 'vs/base/common/severity';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import pkg from 'vs/platform/node/package';
|
||||
import * as path from 'path';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry';
|
||||
import { IMessage, IExtensionDescription, IExtensionsStatus, IExtensionService, ExtensionPointContribution, ActivationTimes } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions, getGloballyDisabledExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionsRegistry, ExtensionPoint, IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint } from 'vs/platform/extensions/common/extensionsRegistry';
|
||||
import { ExtensionScanner, ILog } from 'vs/workbench/services/extensions/electron-browser/extensionPoints';
|
||||
import { IMessageService } from 'vs/platform/message/common/message';
|
||||
import { ProxyIdentifier } from 'vs/workbench/services/thread/common/threadService';
|
||||
import { ExtHostContext, ExtHostExtensionServiceShape, IExtHostContext, MainContext } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ExtensionHostProcessWorker } from 'vs/workbench/services/extensions/electron-browser/extensionHost';
|
||||
import { MainThreadService } from 'vs/workbench/services/thread/electron-browser/threadService';
|
||||
import { Barrier } from 'vs/workbench/services/extensions/node/barrier';
|
||||
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 { Action } from 'vs/base/common/actions';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
const SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions'));
|
||||
|
||||
function messageWithSource(msg: IMessage): string {
|
||||
return messageWithSource2(msg.source, msg.message);
|
||||
}
|
||||
|
||||
function messageWithSource2(source: string, message: string): string {
|
||||
if (source) {
|
||||
return `[${source}]: ${message}`;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
const NO_OP_VOID_PROMISE = TPromise.as<void>(void 0);
|
||||
|
||||
export class ExtensionService implements IExtensionService {
|
||||
public _serviceBrand: any;
|
||||
|
||||
private _registry: ExtensionDescriptionRegistry;
|
||||
private readonly _barrier: Barrier;
|
||||
private readonly _isDev: boolean;
|
||||
private readonly _extensionsStatus: { [id: string]: IExtensionsStatus };
|
||||
private _allRequestedActivateEvents: { [activationEvent: string]: boolean; };
|
||||
|
||||
|
||||
// --- Members used per extension host process
|
||||
|
||||
/**
|
||||
* A map of already activated events to speed things up if the same activation event is triggered multiple times.
|
||||
*/
|
||||
private _extensionHostProcessFinishedActivateEvents: { [activationEvent: string]: boolean; };
|
||||
private _extensionHostProcessActivationTimes: { [id: string]: ActivationTimes; };
|
||||
private _extensionHostProcessWorker: ExtensionHostProcessWorker;
|
||||
private _extensionHostProcessThreadService: MainThreadService;
|
||||
private _extensionHostProcessCustomers: IDisposable[];
|
||||
/**
|
||||
* 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(
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IMessageService private readonly _messageService: IMessageService,
|
||||
@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
|
||||
) {
|
||||
this._registry = null;
|
||||
this._barrier = new Barrier();
|
||||
this._isDev = !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment;
|
||||
this._extensionsStatus = {};
|
||||
this._allRequestedActivateEvents = Object.create(null);
|
||||
|
||||
this._extensionHostProcessFinishedActivateEvents = Object.create(null);
|
||||
this._extensionHostProcessActivationTimes = Object.create(null);
|
||||
this._extensionHostProcessWorker = null;
|
||||
this._extensionHostProcessThreadService = null;
|
||||
this._extensionHostProcessCustomers = [];
|
||||
this._extensionHostProcessProxy = null;
|
||||
|
||||
this._startExtensionHostProcess([]);
|
||||
this._scanAndHandleExtensions();
|
||||
}
|
||||
|
||||
public restartExtensionHost(): void {
|
||||
this._stopExtensionHostProcess();
|
||||
this._startExtensionHostProcess(Object.keys(this._allRequestedActivateEvents));
|
||||
}
|
||||
|
||||
private _stopExtensionHostProcess(): void {
|
||||
this._extensionHostProcessFinishedActivateEvents = Object.create(null);
|
||||
this._extensionHostProcessActivationTimes = Object.create(null);
|
||||
if (this._extensionHostProcessWorker) {
|
||||
this._extensionHostProcessWorker.dispose();
|
||||
this._extensionHostProcessWorker = null;
|
||||
}
|
||||
if (this._extensionHostProcessThreadService) {
|
||||
this._extensionHostProcessThreadService.dispose();
|
||||
this._extensionHostProcessThreadService = null;
|
||||
}
|
||||
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._extensionHostProcessCustomers = [];
|
||||
this._extensionHostProcessProxy = null;
|
||||
}
|
||||
|
||||
private _startExtensionHostProcess(initialActivationEvents: string[]): void {
|
||||
this._stopExtensionHostProcess();
|
||||
|
||||
this._extensionHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, this);
|
||||
this._extensionHostProcessWorker.onCrashed(([code, signal]) => this._onExtensionHostCrashed(code, signal));
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
private _onExtensionHostCrashed(code: number, signal: string): void {
|
||||
const openDevTools = new Action('openDevTools', nls.localize('devTools', "Developer Tools"), '', true, (): TPromise<boolean> => {
|
||||
return this._windowService.openDevTools().then(() => false);
|
||||
});
|
||||
|
||||
const restart = new Action('restart', nls.localize('restart', "Restart Extension Host"), '', true, (): TPromise<boolean> => {
|
||||
this._messageService.hideAll();
|
||||
this._startExtensionHostProcess(Object.keys(this._allRequestedActivateEvents));
|
||||
return TPromise.as(true);
|
||||
});
|
||||
|
||||
console.error('Extension host terminated unexpectedly. Code: ', code, ' Signal: ', signal);
|
||||
this._stopExtensionHostProcess();
|
||||
|
||||
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.");
|
||||
}
|
||||
this._messageService.show(Severity.Error, {
|
||||
message: message,
|
||||
actions: [
|
||||
openDevTools,
|
||||
restart
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
private _createExtensionHostCustomers(protocol: IMessagePassingProtocol): ExtHostExtensionServiceShape {
|
||||
|
||||
this._extensionHostProcessThreadService = this._instantiationService.createInstance(MainThreadService, protocol);
|
||||
const extHostContext: IExtHostContext = this._extensionHostProcessThreadService;
|
||||
|
||||
// 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._extensionHostProcessThreadService.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) => MainContext[key]);
|
||||
this._extensionHostProcessThreadService.assertRegistered(expected);
|
||||
|
||||
return this._extensionHostProcessThreadService.get(ExtHostContext.ExtHostExtensionService);
|
||||
}
|
||||
|
||||
// ---- begin IExtensionService
|
||||
|
||||
public activateByEvent(activationEvent: string): TPromise<void> {
|
||||
if (this._barrier.isOpen()) {
|
||||
// Extensions have been scanned and interpreted
|
||||
|
||||
if (!this._registry.containsActivationEvent(activationEvent)) {
|
||||
// There is no extension that is interested in this activation event
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
|
||||
// Record the fact that this activationEvent was requested (in case of a restart)
|
||||
this._allRequestedActivateEvents[activationEvent] = true;
|
||||
|
||||
return this._activateByEvent(activationEvent);
|
||||
} else {
|
||||
// Extensions have not been scanned yet.
|
||||
|
||||
// Record the fact that this activationEvent was requested (in case of a restart)
|
||||
this._allRequestedActivateEvents[activationEvent] = true;
|
||||
|
||||
return this._barrier.wait().then(() => this._activateByEvent(activationEvent));
|
||||
}
|
||||
}
|
||||
|
||||
protected _activateByEvent(activationEvent: string): TPromise<void> {
|
||||
if (this._extensionHostProcessFinishedActivateEvents[activationEvent]) {
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
return this._extensionHostProcessProxy.then((proxy) => {
|
||||
return proxy.value.$activateByEvent(activationEvent);
|
||||
}).then(() => {
|
||||
this._extensionHostProcessFinishedActivateEvents[activationEvent] = true;
|
||||
});
|
||||
}
|
||||
|
||||
public onReady(): TPromise<boolean> {
|
||||
return this._barrier.wait();
|
||||
}
|
||||
|
||||
public getExtensions(): TPromise<IExtensionDescription[]> {
|
||||
return this.onReady().then(() => {
|
||||
return this._registry.getAllExtensionDescriptions();
|
||||
});
|
||||
}
|
||||
|
||||
public readExtensionPointContributions<T>(extPoint: IExtensionPoint<T>): TPromise<ExtensionPointContribution<T>[]> {
|
||||
return this.onReady().then(() => {
|
||||
let availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
|
||||
let result: ExtensionPointContribution<T>[] = [], resultLen = 0;
|
||||
for (let i = 0, len = availableExtensions.length; i < len; i++) {
|
||||
let desc = availableExtensions[i];
|
||||
|
||||
if (desc.contributes && hasOwnProperty.call(desc.contributes, extPoint.name)) {
|
||||
result[resultLen++] = new ExtensionPointContribution<T>(desc, desc.contributes[extPoint.name]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public getExtensionsStatus(): { [id: string]: IExtensionsStatus; } {
|
||||
return this._extensionsStatus;
|
||||
}
|
||||
|
||||
public getExtensionsActivationTimes(): { [id: string]: ActivationTimes; } {
|
||||
return this._extensionHostProcessActivationTimes;
|
||||
}
|
||||
|
||||
// ---- end IExtensionService
|
||||
|
||||
// --- impl
|
||||
|
||||
private _scanAndHandleExtensions(): void {
|
||||
|
||||
const log = new Logger((severity, source, message) => {
|
||||
this._logOrShowMessage(severity, this._isDev ? messageWithSource2(source, message) : message);
|
||||
});
|
||||
|
||||
ExtensionService._scanInstalledExtensions(this._environmentService, log).then((installedExtensions) => {
|
||||
const disabledExtensions = [
|
||||
...getGloballyDisabledExtensions(this._extensionEnablementService, this._storageService, installedExtensions),
|
||||
...this._extensionEnablementService.getWorkspaceDisabledExtensions()
|
||||
];
|
||||
|
||||
this._telemetryService.publicLog('extensionsScanned', {
|
||||
totalCount: installedExtensions.length,
|
||||
disabledCount: disabledExtensions.length
|
||||
});
|
||||
|
||||
if (disabledExtensions.length === 0) {
|
||||
return installedExtensions;
|
||||
}
|
||||
return installedExtensions.filter(e => disabledExtensions.every(id => !areSameExtensions({ id }, e)));
|
||||
|
||||
}).then((extensionDescriptions) => {
|
||||
this._registry = new ExtensionDescriptionRegistry(extensionDescriptions);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
this._barrier.open();
|
||||
});
|
||||
}
|
||||
|
||||
private _handleExtensionPointMessage(msg: IMessage) {
|
||||
|
||||
if (!this._extensionsStatus[msg.source]) {
|
||||
this._extensionsStatus[msg.source] = { messages: [] };
|
||||
}
|
||||
this._extensionsStatus[msg.source].messages.push(msg);
|
||||
|
||||
if (msg.source === this._environmentService.extensionDevelopmentPath) {
|
||||
// This message is about the extension currently being developed
|
||||
this._showMessageToUser(msg.type, messageWithSource(msg));
|
||||
} else {
|
||||
this._logMessageInConsole(msg.type, messageWithSource(msg));
|
||||
}
|
||||
|
||||
if (!this._isDev && msg.extensionId) {
|
||||
const { type, extensionId, extensionPointId, message } = msg;
|
||||
this._telemetryService.publicLog('extensionsMessage', {
|
||||
type, extensionId, extensionPointId, message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static _scanInstalledExtensions(environmentService: IEnvironmentService, log: ILog): TPromise<IExtensionDescription[]> {
|
||||
const version = pkg.version;
|
||||
const builtinExtensions = ExtensionScanner.scanExtensions(version, log, SystemExtensionsRoot, true);
|
||||
const userExtensions = environmentService.disableExtensions || !environmentService.extensionsPath ? TPromise.as([]) : ExtensionScanner.scanExtensions(version, log, environmentService.extensionsPath, false);
|
||||
const developedExtensions = environmentService.disableExtensions || !environmentService.isExtensionDevelopment ? TPromise.as([]) : ExtensionScanner.scanOneOrMultipleExtensions(version, log, environmentService.extensionDevelopmentPath, false);
|
||||
|
||||
return TPromise.join([builtinExtensions, userExtensions, developedExtensions]).then<IExtensionDescription[]>((extensionDescriptions: IExtensionDescription[][]) => {
|
||||
const builtinExtensions = extensionDescriptions[0];
|
||||
const userExtensions = extensionDescriptions[1];
|
||||
const developedExtensions = extensionDescriptions[2];
|
||||
|
||||
let result: { [extensionId: string]: IExtensionDescription; } = {};
|
||||
builtinExtensions.forEach((builtinExtension) => {
|
||||
result[builtinExtension.id] = builtinExtension;
|
||||
});
|
||||
userExtensions.forEach((userExtension) => {
|
||||
if (result.hasOwnProperty(userExtension.id)) {
|
||||
log.warn(userExtension.extensionFolderPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[userExtension.id].extensionFolderPath, userExtension.extensionFolderPath));
|
||||
}
|
||||
result[userExtension.id] = userExtension;
|
||||
});
|
||||
developedExtensions.forEach(developedExtension => {
|
||||
log.info('', nls.localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionFolderPath));
|
||||
if (result.hasOwnProperty(developedExtension.id)) {
|
||||
log.warn(developedExtension.extensionFolderPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[developedExtension.id].extensionFolderPath, developedExtension.extensionFolderPath));
|
||||
}
|
||||
result[developedExtension.id] = developedExtension;
|
||||
});
|
||||
|
||||
return Object.keys(result).map(name => result[name]);
|
||||
}).then(null, err => {
|
||||
log.error('', err);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
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++) {
|
||||
let desc = availableExtensions[i];
|
||||
|
||||
if (desc.contributes && hasOwnProperty.call(desc.contributes, extensionPoint.name)) {
|
||||
users[usersLen++] = {
|
||||
description: desc,
|
||||
value: desc.contributes[extensionPoint.name],
|
||||
collector: new ExtensionMessageCollector(messageHandler, desc, extensionPoint.name)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extensionPoint.acceptUsers(users);
|
||||
}
|
||||
|
||||
private _showMessageToUser(severity: Severity, msg: string): void {
|
||||
if (severity === Severity.Error || severity === Severity.Warning) {
|
||||
this._messageService.show(severity, msg);
|
||||
} else {
|
||||
this._logMessageInConsole(severity, msg);
|
||||
}
|
||||
}
|
||||
|
||||
private _logMessageInConsole(severity: Severity, msg: string): void {
|
||||
if (severity === Severity.Error) {
|
||||
console.error(msg);
|
||||
} else if (severity === Severity.Warning) {
|
||||
console.warn(msg);
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// -- called by extension host
|
||||
|
||||
public _logOrShowMessage(severity: Severity, msg: string): void {
|
||||
if (this._isDev) {
|
||||
this._showMessageToUser(severity, msg);
|
||||
} else {
|
||||
this._logMessageInConsole(severity, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public _onExtensionActivated(extensionId: string, startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number): void {
|
||||
this._extensionHostProcessActivationTimes[extensionId] = new ActivationTimes(startup, codeLoadingTime, activateCallTime, activateResolvedTime);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
39
src/vs/workbench/services/extensions/node/barrier.ts
Normal file
39
src/vs/workbench/services/extensions/node/barrier.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* A barrier that is initially closed and then becomes opened permanently.
|
||||
*/
|
||||
export class Barrier {
|
||||
|
||||
private _isOpen: boolean;
|
||||
private _promise: TPromise<boolean>;
|
||||
private _completePromise: (v: boolean) => void;
|
||||
|
||||
constructor() {
|
||||
this._isOpen = false;
|
||||
this._promise = new TPromise<boolean>((c, e, p) => {
|
||||
this._completePromise = c;
|
||||
}, () => {
|
||||
console.warn('You should really not try to cancel this ready promise!');
|
||||
});
|
||||
}
|
||||
|
||||
public isOpen(): boolean {
|
||||
return this._isOpen;
|
||||
}
|
||||
|
||||
public open(): void {
|
||||
this._isOpen = true;
|
||||
this._completePromise(true);
|
||||
}
|
||||
|
||||
public wait(): TPromise<boolean> {
|
||||
return this._promise;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/platform/extensions/common/extensions';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
|
||||
export class ExtensionDescriptionRegistry {
|
||||
private _extensionsMap: { [extensionId: string]: IExtensionDescription; };
|
||||
private _extensionsArr: IExtensionDescription[];
|
||||
private _activationMap: { [activationEvent: string]: IExtensionDescription[]; };
|
||||
|
||||
constructor(extensionDescriptions: IExtensionDescription[]) {
|
||||
this._extensionsMap = {};
|
||||
this._extensionsArr = [];
|
||||
this._activationMap = {};
|
||||
|
||||
for (let i = 0, len = extensionDescriptions.length; i < len; i++) {
|
||||
let extensionDescription = extensionDescriptions[i];
|
||||
|
||||
if (hasOwnProperty.call(this._extensionsMap, extensionDescription.id)) {
|
||||
// No overwriting allowed!
|
||||
console.error('Extension `' + extensionDescription.id + '` is already registered');
|
||||
continue;
|
||||
}
|
||||
|
||||
this._extensionsMap[extensionDescription.id] = extensionDescription;
|
||||
this._extensionsArr.push(extensionDescription);
|
||||
|
||||
if (Array.isArray(extensionDescription.activationEvents)) {
|
||||
for (let j = 0, lenJ = extensionDescription.activationEvents.length; j < lenJ; j++) {
|
||||
let activationEvent = extensionDescription.activationEvents[j];
|
||||
this._activationMap[activationEvent] = this._activationMap[activationEvent] || [];
|
||||
this._activationMap[activationEvent].push(extensionDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public containsActivationEvent(activationEvent: string): boolean {
|
||||
return hasOwnProperty.call(this._activationMap, activationEvent);
|
||||
}
|
||||
|
||||
public getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[] {
|
||||
if (!hasOwnProperty.call(this._activationMap, activationEvent)) {
|
||||
return [];
|
||||
}
|
||||
return this._activationMap[activationEvent].slice(0);
|
||||
}
|
||||
|
||||
public getAllExtensionDescriptions(): IExtensionDescription[] {
|
||||
return this._extensionsArr.slice(0);
|
||||
}
|
||||
|
||||
public getExtensionDescription(extensionId: string): IExtensionDescription {
|
||||
if (!hasOwnProperty.call(this._extensionsMap, extensionId)) {
|
||||
return null;
|
||||
}
|
||||
return this._extensionsMap[extensionId];
|
||||
}
|
||||
}
|
||||
110
src/vs/workbench/services/extensions/node/lazyPromise.ts
Normal file
110
src/vs/workbench/services/extensions/node/lazyPromise.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
export class LazyPromise {
|
||||
|
||||
private _onCancel: () => void;
|
||||
|
||||
private _actual: TPromise<any>;
|
||||
private _actualOk: ValueCallback;
|
||||
private _actualErr: ErrorCallback;
|
||||
|
||||
private _hasValue: boolean;
|
||||
private _value: any;
|
||||
|
||||
private _hasErr: boolean;
|
||||
private _err: any;
|
||||
|
||||
private _isCanceled: boolean;
|
||||
|
||||
constructor(onCancel: () => void) {
|
||||
this._onCancel = onCancel;
|
||||
this._actual = null;
|
||||
this._actualOk = null;
|
||||
this._actualErr = null;
|
||||
this._hasValue = false;
|
||||
this._value = null;
|
||||
this._hasErr = false;
|
||||
this._err = null;
|
||||
this._isCanceled = false;
|
||||
}
|
||||
|
||||
private _ensureActual(): TPromise<any> {
|
||||
if (!this._actual) {
|
||||
this._actual = new TPromise<any>((c, e) => {
|
||||
this._actualOk = c;
|
||||
this._actualErr = e;
|
||||
}, this._onCancel);
|
||||
|
||||
if (this._hasValue) {
|
||||
this._actualOk(this._value);
|
||||
}
|
||||
|
||||
if (this._hasErr) {
|
||||
this._actualErr(this._err);
|
||||
}
|
||||
}
|
||||
return this._actual;
|
||||
}
|
||||
|
||||
public resolveOk(value: any): void {
|
||||
if (this._isCanceled || this._hasErr) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._hasValue = true;
|
||||
this._value = value;
|
||||
|
||||
if (this._actual) {
|
||||
this._actualOk(value);
|
||||
}
|
||||
}
|
||||
|
||||
public resolveErr(err: any): void {
|
||||
if (this._isCanceled || this._hasValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._hasErr = true;
|
||||
this._err = err;
|
||||
|
||||
if (this._actual) {
|
||||
this._actualErr(err);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
198
src/vs/workbench/services/extensions/node/rpcProtocol.ts
Normal file
198
src/vs/workbench/services/extensions/node/rpcProtocol.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 marshalling from 'vs/base/common/marshalling';
|
||||
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';
|
||||
|
||||
export interface IDispatcher {
|
||||
invoke(proxyId: string, methodName: string, args: any[]): any;
|
||||
}
|
||||
|
||||
export class RPCProtocol {
|
||||
|
||||
private _isDisposed: boolean;
|
||||
private _bigHandler: IDispatcher;
|
||||
private _lastMessageId: number;
|
||||
private readonly _invokedHandlers: { [req: string]: TPromise<any>; };
|
||||
private readonly _pendingRPCReplies: { [msgId: string]: LazyPromise; };
|
||||
private readonly _multiplexor: RPCMultiplexer;
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol) {
|
||||
this._isDisposed = false;
|
||||
this._bigHandler = null;
|
||||
this._lastMessageId = 0;
|
||||
this._invokedHandlers = Object.create(null);
|
||||
this._pendingRPCReplies = {};
|
||||
this._multiplexor = new RPCMultiplexer(protocol, (msg) => this._receiveOneMessage(msg));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
|
||||
// Release all outstanding promises with a canceled error
|
||||
Object.keys(this._pendingRPCReplies).forEach((msgId) => {
|
||||
const pending = this._pendingRPCReplies[msgId];
|
||||
pending.resolveErr(errors.canceled());
|
||||
});
|
||||
}
|
||||
|
||||
private _receiveOneMessage(rawmsg: string): void {
|
||||
if (this._isDisposed) {
|
||||
console.warn('Received message after being shutdown: ', rawmsg);
|
||||
return;
|
||||
}
|
||||
let msg = marshalling.parse(rawmsg);
|
||||
|
||||
if (msg.seq) {
|
||||
if (!this._pendingRPCReplies.hasOwnProperty(msg.seq)) {
|
||||
console.warn('Got reply to unknown seq');
|
||||
return;
|
||||
}
|
||||
let reply = this._pendingRPCReplies[msg.seq];
|
||||
delete this._pendingRPCReplies[msg.seq];
|
||||
|
||||
if (msg.err) {
|
||||
let err = msg.err;
|
||||
if (msg.err.$isError) {
|
||||
err = new Error();
|
||||
err.name = msg.err.name;
|
||||
err.message = msg.err.message;
|
||||
err.stack = msg.err.stack;
|
||||
}
|
||||
reply.resolveErr(err);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.resolveOk(msg.res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.cancel) {
|
||||
if (this._invokedHandlers[msg.cancel]) {
|
||||
this._invokedHandlers[msg.cancel].cancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.err) {
|
||||
console.error(msg.err);
|
||||
return;
|
||||
}
|
||||
|
||||
let rpcId = msg.rpcId;
|
||||
|
||||
if (!this._bigHandler) {
|
||||
throw new Error('got message before big handler attached!');
|
||||
}
|
||||
|
||||
let req = msg.req;
|
||||
|
||||
this._invokedHandlers[req] = this._invokeHandler(rpcId, msg.method, msg.args);
|
||||
|
||||
this._invokedHandlers[req].then((r) => {
|
||||
delete this._invokedHandlers[req];
|
||||
this._multiplexor.send(MessageFactory.replyOK(req, r));
|
||||
}, (err) => {
|
||||
delete this._invokedHandlers[req];
|
||||
this._multiplexor.send(MessageFactory.replyErr(req, err));
|
||||
});
|
||||
}
|
||||
|
||||
private _invokeHandler(proxyId: string, methodName: string, args: any[]): TPromise<any> {
|
||||
try {
|
||||
return TPromise.as(this._bigHandler.invoke(proxyId, methodName, args));
|
||||
} catch (err) {
|
||||
return TPromise.wrapError(err);
|
||||
}
|
||||
}
|
||||
|
||||
public callOnRemote(proxyId: string, methodName: string, args: any[]): TPromise<any> {
|
||||
if (this._isDisposed) {
|
||||
return TPromise.wrapError<any>(errors.canceled());
|
||||
}
|
||||
|
||||
let req = String(++this._lastMessageId);
|
||||
let result = new LazyPromise(() => {
|
||||
this._multiplexor.send(MessageFactory.cancel(req));
|
||||
});
|
||||
|
||||
this._pendingRPCReplies[req] = result;
|
||||
|
||||
this._multiplexor.send(MessageFactory.request(req, proxyId, methodName, args));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public setDispatcher(handler: IDispatcher): void {
|
||||
this._bigHandler = handler;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
private readonly _protocol: IMessagePassingProtocol;
|
||||
private readonly _sendAccumulatedBound: () => void;
|
||||
|
||||
private _messagesToSend: string[];
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol, onMessage: (msg: string) => void) {
|
||||
this._protocol = protocol;
|
||||
this._sendAccumulatedBound = this._sendAccumulated.bind(this);
|
||||
|
||||
this._messagesToSend = [];
|
||||
|
||||
this._protocol.onMessage(data => {
|
||||
for (let i = 0, len = data.length; i < len; i++) {
|
||||
onMessage(data[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageFactory {
|
||||
public static cancel(req: string): string {
|
||||
return `{"cancel":"${req}"}`;
|
||||
}
|
||||
|
||||
public static request(req: string, rpcId: string, method: string, args: any[]): string {
|
||||
return `{"req":"${req}","rpcId":"${rpcId}","method":"${method}","args":${marshalling.stringify(args)}}`;
|
||||
}
|
||||
|
||||
public static replyOK(req: string, res: any): string {
|
||||
if (typeof res === 'undefined') {
|
||||
return `{"seq":"${req}"}`;
|
||||
}
|
||||
return `{"seq":"${req}","res":${marshalling.stringify(res)}}`;
|
||||
}
|
||||
|
||||
public static replyErr(req: string, err: any): string {
|
||||
if (typeof err === 'undefined') {
|
||||
return `{"seq":"${req}","err":null}`;
|
||||
}
|
||||
return `{"seq":"${req}","err":${marshalling.stringify(errors.transformErrorForSerialization(err))}}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user