提交 72089179 编写于 作者: S Sandeep Somavarapu

#13357 Separate extension host process starting from thread service

上级 d2691646
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import pkg from 'vs/platform/package';
import paths = require('vs/base/common/paths');
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 } 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 { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWindowService } from 'vs/workbench/services/window/electron-browser/windowService';
import { ChildProcess, fork } from 'child_process';
import { ipcRenderer as ipc } from 'electron';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ReloadWindowAction } from 'vs/workbench/electron-browser/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionDescription, IMessage } from 'vs/platform/extensions/common/extensions';
import { ExtensionScanner, MessagesCollector } from 'vs/workbench/node/extensionPoints';
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
import Event, { Emitter } from 'vs/base/common/event';
export const EXTENSION_LOG_BROADCAST_CHANNEL = 'vscode:extensionLog';
export const EXTENSION_ATTACH_BROADCAST_CHANNEL = 'vscode:extensionAttach';
export const EXTENSION_TERMINATE_BROADCAST_CHANNEL = 'vscode:extensionTerminate';
const DIRNAME = URI.parse(require.toUrl('./')).fsPath;
const BASE_PATH = paths.normalize(paths.join(DIRNAME, '../../../..'));
const BUILTIN_EXTENSIONS_PATH = paths.join(BASE_PATH, 'extensions');
export interface ILogEntry {
type: string;
severity: string;
arguments: any;
}
export class ExtensionHostProcessWorker {
private initializeExtensionHostProcess: TPromise<ChildProcess>;
private extensionHostProcessHandle: ChildProcess;
private extensionHostProcessReady: boolean;
private initializeTimer: number;
private lastExtensionHostError: string;
private unsentMessages: any[];
private terminating: boolean;
private isExtensionDevelopmentHost: boolean;
private isExtensionDevelopmentTestFromCli: boolean;
private isExtensionDevelopmentDebugging: boolean;
private _onMessage = new Emitter<any>();
public get onMessage(): Event<any> {
return this._onMessage.event;
}
constructor(
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IMessageService private messageService: IMessageService,
@IWindowService private windowService: IWindowService,
@ILifecycleService lifecycleService: ILifecycleService,
@IInstantiationService private instantiationService: IInstantiationService,
@IEnvironmentService private environmentService: IEnvironmentService
) {
// handle extension host lifecycle a bit special when we know we are developing an extension that runs inside
this.isExtensionDevelopmentHost = !!environmentService.extensionDevelopmentPath;
this.isExtensionDevelopmentDebugging = !!environmentService.debugExtensionHost.break;
this.isExtensionDevelopmentTestFromCli = this.isExtensionDevelopmentHost && !!environmentService.extensionTestsPath && !environmentService.debugExtensionHost.break;
this.unsentMessages = [];
this.extensionHostProcessReady = false;
lifecycleService.onWillShutdown(this._onWillShutdown, this);
lifecycleService.onShutdown(() => this.terminate());
}
public start(): void {
let opts: any = {
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.getWindowId())
}),
// 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) is taken down in a brutal fashion by the OS
detached: !!isWindows,
};
// Help in case we fail to start it
if (!this.environmentService.isBuilt || this.isExtensionDevelopmentHost) {
this.initializeTimer = setTimeout(() => {
const msg = this.isExtensionDevelopmentDebugging ? 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
this.initializeExtensionHostProcess = this.doInitializeExtensionHostProcess(opts);
}
public get messagingProtocol(): IMessagePassingProtocol {
return this;
}
private doInitializeExtensionHostProcess(opts: any): TPromise<ChildProcess> {
return new TPromise<ChildProcess>((c, e) => {
// Resolve additional execution args (e.g. debug)
this.resolveDebugPort(this.environmentService.debugExtensionHost.port).then(port => {
if (port) {
opts.execArgv = ['--nolazy', (this.isExtensionDevelopmentDebugging ? '--debug-brk=' : '--debug=') + port];
}
// Run Extension Host as fork of current process
this.extensionHostProcessHandle = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts);
// Notify debugger that we are ready to attach to the process if we run a development extension
if (this.isExtensionDevelopmentHost && port) {
this.windowService.broadcast({
channel: EXTENSION_ATTACH_BROADCAST_CHANNEL,
payload: { port }
}, this.environmentService.extensionDevelopmentPath /* target */);
}
// Messages from Extension host
this.extensionHostProcessHandle.on('message', msg => {
if (this.onMessaage(msg)) {
c(this.extensionHostProcessHandle);
}
});
// Lifecycle
let onExit = () => this.terminate();
process.once('exit', onExit);
this.extensionHostProcessHandle.on('error', (err) => this.onError(err));
this.extensionHostProcessHandle.on('exit', (code: any, signal: any) => this.onExit(code, signal, onExit));
});
}, () => this.terminate());
}
private resolveDebugPort(extensionHostPort: number): TPromise<number> {
if (typeof extensionHostPort !== 'number') {
return TPromise.wrap(void 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');
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.isExtensionDevelopmentDebugging) {
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);
});
});
}
// @return `true` if ready
private onMessaage(msg: any): boolean {
// 1) Host is ready to receive messages, initialize it
if (msg === 'ready') {
this.initializeExtensionHost();
return false;
}
// 2) Host is initialized
if (msg === 'initialized') {
this.unsentMessages.forEach(m => this.send(m));
this.unsentMessages = [];
this.extensionHostProcessReady = true;
return true;
}
// Support logging from extension host
if (msg && (<ILogEntry>msg).type === '__$console') {
this.logExtensionHostMessage(<ILogEntry>msg);
return false;
}
// Any other message emits event
this._onMessage.fire(msg);
return false;
}
private initializeExtensionHost() {
if (this.initializeTimer) {
window.clearTimeout(this.initializeTimer);
}
this.scanExtensions().then(extensionDescriptors => {
let initPayload = stringify({
parentPid: process.pid,
environment: {
appSettingsHome: this.environmentService.appSettingsHome,
disableExtensions: this.environmentService.disableExtensions,
userExtensionsHome: this.environmentService.extensionsPath,
extensionDevelopmentPath: this.environmentService.extensionDevelopmentPath,
extensionTestsPath: this.environmentService.extensionTestsPath
},
contextService: {
workspace: this.contextService.getWorkspace()
},
extensions: extensionDescriptors
});
this.extensionHostProcessHandle.send(initPayload);
});
}
private scanExtensions(): TPromise<IExtensionDescription[]> {
const collector = new MessagesCollector();
const version = pkg.version;
const builtinExtensions = ExtensionScanner.scanExtensions(version, collector, BUILTIN_EXTENSIONS_PATH, true);
const userExtensions = this.environmentService.disableExtensions || !this.environmentService.extensionsPath ? TPromise.as([]) : ExtensionScanner.scanExtensions(version, collector, this.environmentService.extensionsPath, false);
const developedExtensions = this.environmentService.disableExtensions || !this.environmentService.extensionDevelopmentPath ? TPromise.as([]) : ExtensionScanner.scanOneOrMultipleExtensions(version, collector, this.environmentService.extensionDevelopmentPath, false);
const isDev = !this.environmentService.isBuilt || !!this.environmentService.extensionDevelopmentPath;
return TPromise.join([builtinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => {
let builtinExtensions = extensionDescriptions[0];
let userExtensions = extensionDescriptions[1];
let developedExtensions = extensionDescriptions[2];
let result: { [extensionId: string]: IExtensionDescription; } = {};
builtinExtensions.forEach((builtinExtension) => {
result[builtinExtension.id] = builtinExtension;
});
userExtensions.forEach((userExtension) => {
if (result.hasOwnProperty(userExtension.id)) {
collector.warn(userExtension.extensionFolderPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[userExtension.id].extensionFolderPath, userExtension.extensionFolderPath));
}
result[userExtension.id] = userExtension;
});
developedExtensions.forEach(developedExtension => {
collector.info('', nls.localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionFolderPath));
if (result.hasOwnProperty(developedExtension.id)) {
collector.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 => {
collector.error('', err);
return [];
}).then(extensions => {
collector.getMessages().forEach(entry => this._handleMessage(entry, isDev));
return extensions;
});
}
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.isExtensionDevelopmentTestFromCli) {
console[logEntry.severity].apply(console, consoleArgs);
}
// Log on main side if running tests from cli
if (this.isExtensionDevelopmentTestFromCli) {
ipc.send('vscode:log', logEntry);
}
// Broadcast to other windows if we are in development mode
else if (!this.environmentService.isBuilt || this.isExtensionDevelopmentHost) {
this.windowService.broadcast({
channel: EXTENSION_LOG_BROADCAST_CHANNEL,
payload: logEntry
}, this.environmentService.extensionDevelopmentPath /* target */);
}
}
private onError(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 onExit(code: any, signal: any, onProcessExit: any): void {
process.removeListener('exit', onProcessExit);
if (!this.terminating) {
// Unexpected termination
if (!this.isExtensionDevelopmentHost) {
this.messageService.show(Severity.Error, {
message: nls.localize('extensionHostProcess.crash', "Extension host terminated unexpectedly. Please reload the window to recover."),
actions: [this.instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, ReloadWindowAction.LABEL)]
});
console.error('Extension host terminated unexpectedly. Code: ', code, ' Signal: ', signal);
}
// Expected development extension termination: When the extension host goes down we also shutdown the window
else if (!this.isExtensionDevelopmentTestFromCli) {
this.windowService.getWindow().close();
}
// When CLI testing make sure to exit with proper exit code
else {
ipc.send('vscode:exit', code);
}
}
}
public send(msg: any): void {
if (this.extensionHostProcessReady) {
this.extensionHostProcessHandle.send(msg);
} else if (this.initializeExtensionHostProcess) {
this.initializeExtensionHostProcess.done(p => p.send(msg));
} else {
this.unsentMessages.push(msg);
}
}
public terminate(): void {
this.terminating = true;
if (this.extensionHostProcessHandle) {
this.extensionHostProcessHandle.send({
type: '__$terminate'
});
}
}
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.isExtensionDevelopmentHost && !this.isExtensionDevelopmentTestFromCli && !this.isExtensionDevelopmentDebugging) {
this.windowService.broadcast({
channel: EXTENSION_TERMINATE_BROADCAST_CHANNEL,
payload: true
}, this.environmentService.extensionDevelopmentPath /* target */);
event.veto(TPromise.timeout(100 /* wait a bit for IPC to get delivered */).then(() => false));
}
}
private _handleMessage(message: IMessage, isDev: boolean): void {
let messageShown = false;
if (message.type === Severity.Error || message.type === Severity.Warning) {
if (isDev) {
// Only show nasty intrusive messages if doing extension development.
this.messageService.show(message.type, (message.source ? '[' + message.source + ']: ' : '') + message.message);
messageShown = true;
}
}
if (!messageShown) {
switch (message.type) {
case Severity.Error:
console.error(message);
break;
case Severity.Warning:
console.warn(message);
break;
default:
console.log(message);
}
}
}
}
\ No newline at end of file
...@@ -82,6 +82,7 @@ import { URLChannelClient } from 'vs/platform/url/common/urlIpc'; ...@@ -82,6 +82,7 @@ import { URLChannelClient } from 'vs/platform/url/common/urlIpc';
import { IURLService } from 'vs/platform/url/common/url'; import { IURLService } from 'vs/platform/url/common/url';
import { ReloadWindowAction } from 'vs/workbench/electron-browser/actions'; import { ReloadWindowAction } from 'vs/workbench/electron-browser/actions';
import { WorkspaceConfigurationService } from 'vs/workbench/services/configuration/node/configurationService'; import { WorkspaceConfigurationService } from 'vs/workbench/services/configuration/node/configurationService';
import { ExtensionHostProcessWorker } from 'vs/workbench/electron-browser/extensionHost';
// self registering services // self registering services
import 'vs/platform/opener/browser/opener.contribution'; import 'vs/platform/opener/browser/opener.contribution';
...@@ -290,7 +291,8 @@ export class WorkbenchShell { ...@@ -290,7 +291,8 @@ export class WorkbenchShell {
this.toUnbind.push(lifecycleService.onShutdown(() => disposables.dispose())); this.toUnbind.push(lifecycleService.onShutdown(() => disposables.dispose()));
serviceCollection.set(ILifecycleService, lifecycleService); serviceCollection.set(ILifecycleService, lifecycleService);
this.threadService = instantiationService.createInstance(MainThreadService); const extensionHostProcessWorker = this.startExtensionHost(instantiationService);
this.threadService = instantiationService.createInstance(MainThreadService, extensionHostProcessWorker.messagingProtocol);
serviceCollection.set(IThreadService, this.threadService); serviceCollection.set(IThreadService, this.threadService);
const extensionService = instantiationService.createInstance(MainProcessExtensionService); const extensionService = instantiationService.createInstance(MainProcessExtensionService);
...@@ -451,6 +453,12 @@ export class WorkbenchShell { ...@@ -451,6 +453,12 @@ export class WorkbenchShell {
this.workbench.layout(); this.workbench.layout();
} }
private startExtensionHost(instantiationService: InstantiationService): ExtensionHostProcessWorker {
const extensionHostProcessWorker: ExtensionHostProcessWorker = <ExtensionHostProcessWorker>instantiationService.createInstance(ExtensionHostProcessWorker);
extensionHostProcessWorker.start();
return extensionHostProcessWorker;
}
public joinCreation(): TPromise<boolean> { public joinCreation(): TPromise<boolean> {
return this.workbench.joinCreation(); return this.workbench.joinCreation();
} }
......
...@@ -51,7 +51,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur ...@@ -51,7 +51,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWindowService, IBroadcast } from 'vs/workbench/services/window/electron-browser/windowService'; import { IWindowService, IBroadcast } from 'vs/workbench/services/window/electron-browser/windowService';
import { ILogEntry, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/workbench/services/thread/electron-browser/threadService'; import { ILogEntry, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/workbench/electron-browser/extensionHost';
import { ipcRenderer as ipc } from 'electron'; import { ipcRenderer as ipc } from 'electron';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
......
...@@ -5,80 +5,37 @@ ...@@ -5,80 +5,37 @@
'use strict'; 'use strict';
import * as nls from 'vs/nls';
import pkg from 'vs/platform/package';
import paths = require('vs/base/common/paths');
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { stringify } from 'vs/base/common/marshalling';
import * as objects from 'vs/base/common/objects';
import * as strings from 'vs/base/common/strings'; import * as strings from 'vs/base/common/strings';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base'; import { TPromise } from 'vs/base/common/winjs.base';
import { isWindows } from 'vs/base/common/platform';
import { findFreePort } from 'vs/base/node/ports';
import { IMainProcessExtHostIPC, create } from 'vs/platform/extensions/common/ipcRemoteCom'; import { IMainProcessExtHostIPC, create } from 'vs/platform/extensions/common/ipcRemoteCom';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
import { AbstractThreadService } from 'vs/workbench/services/thread/common/abstractThreadService'; import { AbstractThreadService } from 'vs/workbench/services/thread/common/abstractThreadService';
import { ILifecycleService, ShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWindowService } from 'vs/workbench/services/window/electron-browser/windowService';
import { ChildProcess, fork } from 'child_process';
import { ipcRenderer as ipc } from 'electron';
import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; import { IThreadService } from 'vs/workbench/services/thread/common/threadService';
import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ReloadWindowAction } from 'vs/workbench/electron-browser/actions'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionDescription, IMessage } from 'vs/platform/extensions/common/extensions';
import { ExtensionScanner, MessagesCollector } from 'vs/workbench/node/extensionPoints';
export const EXTENSION_LOG_BROADCAST_CHANNEL = 'vscode:extensionLog';
export const EXTENSION_ATTACH_BROADCAST_CHANNEL = 'vscode:extensionAttach';
export const EXTENSION_TERMINATE_BROADCAST_CHANNEL = 'vscode:extensionTerminate';
const DIRNAME = URI.parse(require.toUrl('./')).fsPath;
const BASE_PATH = paths.normalize(paths.join(DIRNAME, '../../../../../..'));
const BUILTIN_EXTENSIONS_PATH = paths.join(BASE_PATH, 'extensions');
// Enable to see detailed message communication between window and extension host // Enable to see detailed message communication between window and extension host
const logExtensionHostCommunication = false; const logExtensionHostCommunication = false;
export interface ILogEntry {
type: string;
severity: string;
arguments: any;
}
export class MainThreadService extends AbstractThreadService implements IThreadService { export class MainThreadService extends AbstractThreadService implements IThreadService {
public _serviceBrand: any; public _serviceBrand: any;
private extensionHostProcessManager: ExtensionHostProcessManager;
private remoteCom: IMainProcessExtHostIPC; private remoteCom: IMainProcessExtHostIPC;
constructor( constructor(extensionHostMessagingProtocol: IMessagePassingProtocol, @IEnvironmentService environmentService: IEnvironmentService) {
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IMessageService messageService: IMessageService,
@IWindowService windowService: IWindowService,
@IEnvironmentService environmentService: IEnvironmentService,
@ILifecycleService lifecycleService: ILifecycleService,
@IInstantiationService instantiationService: IInstantiationService
) {
super(true); super(true);
this.extensionHostProcessManager = instantiationService.createInstance(ExtensionHostProcessManager);
let logCommunication = logExtensionHostCommunication || environmentService.logExtensionHostCommunication; let logCommunication = logExtensionHostCommunication || environmentService.logExtensionHostCommunication;
// Message: Window --> Extension Host // Message: Window --> Extension Host
this.remoteCom = create((msg) => { this.remoteCom = create((msg) => {
if (logCommunication) { if (logCommunication) {
console.log('%c[Window \u2192 Extension]%c[len: ' + strings.pad(msg.length, 5, ' ') + ']', 'color: darkgreen', 'color: grey', msg); console.log('%c[Window \u2192 Extension]%c[len: ' + strings.pad(msg.length, 5, ' ') + ']', 'color: darkgreen', 'color: grey', msg);
} }
this.extensionHostProcessManager.postMessage(msg); extensionHostMessagingProtocol.send(msg);
}); });
// Message: Extension Host --> Window // Message: Extension Host --> Window
this.extensionHostProcessManager.startExtensionHostProcess((msg) => { extensionHostMessagingProtocol.onMessage((msg) => {
if (logCommunication) { if (logCommunication) {
console.log('%c[Extension \u2192 Window]%c[len: ' + strings.pad(msg.length, 5, ' ') + ']', 'color: darkgreen', 'color: grey', msg); console.log('%c[Extension \u2192 Window]%c[len: ' + strings.pad(msg.length, 5, ' ') + ']', 'color: darkgreen', 'color: grey', msg);
} }
...@@ -87,359 +44,9 @@ export class MainThreadService extends AbstractThreadService implements IThreadS ...@@ -87,359 +44,9 @@ export class MainThreadService extends AbstractThreadService implements IThreadS
}); });
this.remoteCom.setManyHandler(this); this.remoteCom.setManyHandler(this);
lifecycleService.onShutdown(() => this.dispose());
}
public dispose(): void {
this.extensionHostProcessManager.terminate();
} }
protected _callOnRemote(proxyId: string, path: string, args: any[]): TPromise<any> { protected _callOnRemote(proxyId: string, path: string, args: any[]): TPromise<any> {
return this.remoteCom.callOnRemote(proxyId, path, args); return this.remoteCom.callOnRemote(proxyId, path, args);
} }
}
class ExtensionHostProcessManager {
private initializeExtensionHostProcess: TPromise<ChildProcess>;
private extensionHostProcessHandle: ChildProcess;
private extensionHostProcessReady: boolean;
private initializeTimer: number;
private lastExtensionHostError: string;
private unsentMessages: any[];
private terminating: boolean;
private isExtensionDevelopmentHost: boolean;
private isExtensionDevelopmentTestFromCli: boolean;
private isExtensionDevelopmentDebugging: boolean;
constructor(
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IMessageService private messageService: IMessageService,
@IWindowService private windowService: IWindowService,
@ILifecycleService lifecycleService: ILifecycleService,
@IInstantiationService private instantiationService: IInstantiationService,
@IEnvironmentService private environmentService: IEnvironmentService
) {
// handle extension host lifecycle a bit special when we know we are developing an extension that runs inside
this.isExtensionDevelopmentHost = !!environmentService.extensionDevelopmentPath;
this.isExtensionDevelopmentDebugging = !!environmentService.debugExtensionHost.break;
this.isExtensionDevelopmentTestFromCli = this.isExtensionDevelopmentHost && !!environmentService.extensionTestsPath && !environmentService.debugExtensionHost.break;
this.unsentMessages = [];
this.extensionHostProcessReady = false;
lifecycleService.onWillShutdown(this._onWillShutdown, this);
}
public startExtensionHostProcess(onExtensionHostMessage: (msg: any) => void): void {
let opts: any = {
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.getWindowId())
}),
// 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) is taken down in a brutal fashion by the OS
detached: !!isWindows,
onExtensionHostMessage
};
// Help in case we fail to start it
if (!this.environmentService.isBuilt || this.isExtensionDevelopmentHost) {
this.initializeTimer = setTimeout(() => {
const msg = this.isExtensionDevelopmentDebugging ? 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
this.initializeExtensionHostProcess = this.doInitializeExtensionHostProcess(opts);
}
private doInitializeExtensionHostProcess(opts: any): TPromise<ChildProcess> {
return new TPromise<ChildProcess>((c, e) => {
// Resolve additional execution args (e.g. debug)
this.resolveDebugPort(this.environmentService.debugExtensionHost.port).then(port => {
if (port) {
opts.execArgv = ['--nolazy', (this.isExtensionDevelopmentDebugging ? '--debug-brk=' : '--debug=') + port];
}
// Run Extension Host as fork of current process
this.extensionHostProcessHandle = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts);
// Notify debugger that we are ready to attach to the process if we run a development extension
if (this.isExtensionDevelopmentHost && port) {
this.windowService.broadcast({
channel: EXTENSION_ATTACH_BROADCAST_CHANNEL,
payload: { port }
}, this.environmentService.extensionDevelopmentPath /* target */);
}
// Messages from Extension host
this.extensionHostProcessHandle.on('message', msg => {
if (this.onMessaage(msg, opts.onExtensionHostMessage)) {
c(this.extensionHostProcessHandle);
}
});
// Lifecycle
let onExit = () => this.terminate();
process.once('exit', onExit);
this.extensionHostProcessHandle.on('error', (err) => this.onError(err));
this.extensionHostProcessHandle.on('exit', (code: any, signal: any) => this.onExit(code, signal, onExit));
});
}, () => this.terminate());
}
private resolveDebugPort(extensionHostPort: number): TPromise<number> {
if (typeof extensionHostPort !== 'number') {
return TPromise.wrap(void 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');
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.isExtensionDevelopmentDebugging) {
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);
});
});
}
// @return `true` if ready
private onMessaage(msg: any, onExtensionHostMessage: (msg: any) => void): boolean {
// 1) Host is ready to receive messages, initialize it
if (msg === 'ready') {
this.initializeExtensionHost();
return false;
}
// 2) Host is initialized
if (msg === 'initialized') {
this.unsentMessages.forEach(m => this.postMessage(m));
this.unsentMessages = [];
this.extensionHostProcessReady = true;
return true;
}
// Support logging from extension host
if (msg && (<ILogEntry>msg).type === '__$console') {
this.logExtensionHostMessage(<ILogEntry>msg);
return false;
}
// Any other message goes to the callback
onExtensionHostMessage(msg);
return false;
}
private initializeExtensionHost() {
if (this.initializeTimer) {
window.clearTimeout(this.initializeTimer);
}
this.scanExtensions().then(extensionDescriptors => {
let initPayload = stringify({
parentPid: process.pid,
environment: {
appSettingsHome: this.environmentService.appSettingsHome,
disableExtensions: this.environmentService.disableExtensions,
userExtensionsHome: this.environmentService.extensionsPath,
extensionDevelopmentPath: this.environmentService.extensionDevelopmentPath,
extensionTestsPath: this.environmentService.extensionTestsPath
},
contextService: {
workspace: this.contextService.getWorkspace()
},
extensions: extensionDescriptors
});
this.extensionHostProcessHandle.send(initPayload);
});
}
private scanExtensions(): TPromise<IExtensionDescription[]> {
const collector = new MessagesCollector();
const version = pkg.version;
const builtinExtensions = ExtensionScanner.scanExtensions(version, collector, BUILTIN_EXTENSIONS_PATH, true);
const userExtensions = this.environmentService.disableExtensions || !this.environmentService.extensionsPath ? TPromise.as([]) : ExtensionScanner.scanExtensions(version, collector, this.environmentService.extensionsPath, false);
const developedExtensions = this.environmentService.disableExtensions || !this.environmentService.extensionDevelopmentPath ? TPromise.as([]) : ExtensionScanner.scanOneOrMultipleExtensions(version, collector, this.environmentService.extensionDevelopmentPath, false);
const isDev = !this.environmentService.isBuilt || !!this.environmentService.extensionDevelopmentPath;
return TPromise.join([builtinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => {
let builtinExtensions = extensionDescriptions[0];
let userExtensions = extensionDescriptions[1];
let developedExtensions = extensionDescriptions[2];
let result: { [extensionId: string]: IExtensionDescription; } = {};
builtinExtensions.forEach((builtinExtension) => {
result[builtinExtension.id] = builtinExtension;
});
userExtensions.forEach((userExtension) => {
if (result.hasOwnProperty(userExtension.id)) {
collector.warn(userExtension.extensionFolderPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[userExtension.id].extensionFolderPath, userExtension.extensionFolderPath));
}
result[userExtension.id] = userExtension;
});
developedExtensions.forEach(developedExtension => {
collector.info('', nls.localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionFolderPath));
if (result.hasOwnProperty(developedExtension.id)) {
collector.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 => {
collector.error('', err);
return [];
}).then(extensions => {
collector.getMessages().forEach(entry => this._handleMessage(entry, isDev));
return extensions;
});
}
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.isExtensionDevelopmentTestFromCli) {
console[logEntry.severity].apply(console, consoleArgs);
}
// Log on main side if running tests from cli
if (this.isExtensionDevelopmentTestFromCli) {
ipc.send('vscode:log', logEntry);
}
// Broadcast to other windows if we are in development mode
else if (!this.environmentService.isBuilt || this.isExtensionDevelopmentHost) {
this.windowService.broadcast({
channel: EXTENSION_LOG_BROADCAST_CHANNEL,
payload: logEntry
}, this.environmentService.extensionDevelopmentPath /* target */);
}
}
private onError(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 onExit(code: any, signal: any, onProcessExit: any): void {
process.removeListener('exit', onProcessExit);
if (!this.terminating) {
// Unexpected termination
if (!this.isExtensionDevelopmentHost) {
this.messageService.show(Severity.Error, {
message: nls.localize('extensionHostProcess.crash', "Extension host terminated unexpectedly. Please reload the window to recover."),
actions: [this.instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, ReloadWindowAction.LABEL)]
});
console.error('Extension host terminated unexpectedly. Code: ', code, ' Signal: ', signal);
}
// Expected development extension termination: When the extension host goes down we also shutdown the window
else if (!this.isExtensionDevelopmentTestFromCli) {
this.windowService.getWindow().close();
}
// When CLI testing make sure to exit with proper exit code
else {
ipc.send('vscode:exit', code);
}
}
}
public postMessage(msg: any): void {
if (this.extensionHostProcessReady) {
this.extensionHostProcessHandle.send(msg);
} else if (this.initializeExtensionHostProcess) {
this.initializeExtensionHostProcess.done(p => p.send(msg));
} else {
this.unsentMessages.push(msg);
}
}
public terminate(): void {
this.terminating = true;
if (this.extensionHostProcessHandle) {
this.extensionHostProcessHandle.send({
type: '__$terminate'
});
}
}
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.isExtensionDevelopmentHost && !this.isExtensionDevelopmentTestFromCli && !this.isExtensionDevelopmentDebugging) {
this.windowService.broadcast({
channel: EXTENSION_TERMINATE_BROADCAST_CHANNEL,
payload: true
}, this.environmentService.extensionDevelopmentPath /* target */);
event.veto(TPromise.timeout(100 /* wait a bit for IPC to get delivered */).then(() => false));
}
}
private _handleMessage(message: IMessage, isDev: boolean): void {
let messageShown = false;
if (message.type === Severity.Error || message.type === Severity.Warning) {
if (isDev) {
// Only show nasty intrusive messages if doing extension development.
this.messageService.show(message.type, (message.source ? '[' + message.source + ']: ' : '') + message.message);
messageShown = true;
}
}
if (!messageShown) {
switch (message.type) {
case Severity.Error:
console.error(message);
break;
case Severity.Warning:
console.warn(message);
break;
default:
console.log(message);
}
}
}
} }
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册