Skip to content

Commit

Permalink
#13357 Separate extension host process starting from thread service
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 committed Oct 10, 2016
1 parent d269164 commit 7208917
Show file tree
Hide file tree
Showing 4 changed files with 410 additions and 399 deletions.
396 changes: 396 additions & 0 deletions src/vs/workbench/electron-browser/extensionHost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,396 @@
/*---------------------------------------------------------------------------------------------
* 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);
}
}
}
}
Loading

0 comments on commit 7208917

Please sign in to comment.