From 72089179ddbda33ada2ba526ad1255fd21d0fcc0 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 10 Oct 2016 17:09:03 +0200 Subject: [PATCH] #13357 Separate extension host process starting from thread service --- .../electron-browser/extensionHost.ts | 396 +++++++++++++++++ src/vs/workbench/electron-browser/shell.ts | 10 +- .../debug/electron-browser/debugService.ts | 2 +- .../thread/electron-browser/threadService.ts | 401 +----------------- 4 files changed, 410 insertions(+), 399 deletions(-) create mode 100644 src/vs/workbench/electron-browser/extensionHost.ts diff --git a/src/vs/workbench/electron-browser/extensionHost.ts b/src/vs/workbench/electron-browser/extensionHost.ts new file mode 100644 index 0000000000000..abffec1b85aa7 --- /dev/null +++ b/src/vs/workbench/electron-browser/extensionHost.ts @@ -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; + 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(); + public get onMessage(): Event { + 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 { + return new TPromise((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 { + if (typeof extensionHostPort !== 'number') { + return TPromise.wrap(void 0); + } + return new TPromise((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 && (msg).type === '__$console') { + this.logExtensionHostMessage(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 { + 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 diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 71ab367ee01a1..f0cd985393b76 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -82,6 +82,7 @@ import { URLChannelClient } from 'vs/platform/url/common/urlIpc'; import { IURLService } from 'vs/platform/url/common/url'; import { ReloadWindowAction } from 'vs/workbench/electron-browser/actions'; import { WorkspaceConfigurationService } from 'vs/workbench/services/configuration/node/configurationService'; +import { ExtensionHostProcessWorker } from 'vs/workbench/electron-browser/extensionHost'; // self registering services import 'vs/platform/opener/browser/opener.contribution'; @@ -290,7 +291,8 @@ export class WorkbenchShell { this.toUnbind.push(lifecycleService.onShutdown(() => disposables.dispose())); 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); const extensionService = instantiationService.createInstance(MainProcessExtensionService); @@ -451,6 +453,12 @@ export class WorkbenchShell { this.workbench.layout(); } + private startExtensionHost(instantiationService: InstantiationService): ExtensionHostProcessWorker { + const extensionHostProcessWorker: ExtensionHostProcessWorker = instantiationService.createInstance(ExtensionHostProcessWorker); + extensionHostProcessWorker.start(); + return extensionHostProcessWorker; + } + public joinCreation(): TPromise { return this.workbench.joinCreation(); } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index d876e964c4507..18ea479896106 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -51,7 +51,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; 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 { Client } from 'vs/base/parts/ipc/node/ipc.cp'; diff --git a/src/vs/workbench/services/thread/electron-browser/threadService.ts b/src/vs/workbench/services/thread/electron-browser/threadService.ts index f6f2dfd34fba9..2e080fbb5c9a4 100644 --- a/src/vs/workbench/services/thread/electron-browser/threadService.ts +++ b/src/vs/workbench/services/thread/electron-browser/threadService.ts @@ -5,80 +5,37 @@ '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 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 { 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 { 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 { 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'; - -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'); +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; // Enable to see detailed message communication between window and extension host const logExtensionHostCommunication = false; -export interface ILogEntry { - type: string; - severity: string; - arguments: any; -} - export class MainThreadService extends AbstractThreadService implements IThreadService { public _serviceBrand: any; - private extensionHostProcessManager: ExtensionHostProcessManager; private remoteCom: IMainProcessExtHostIPC; - constructor( - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IMessageService messageService: IMessageService, - @IWindowService windowService: IWindowService, - @IEnvironmentService environmentService: IEnvironmentService, - @ILifecycleService lifecycleService: ILifecycleService, - @IInstantiationService instantiationService: IInstantiationService - ) { + constructor(extensionHostMessagingProtocol: IMessagePassingProtocol, @IEnvironmentService environmentService: IEnvironmentService) { super(true); - this.extensionHostProcessManager = instantiationService.createInstance(ExtensionHostProcessManager); - let logCommunication = logExtensionHostCommunication || environmentService.logExtensionHostCommunication; - // Message: Window --> Extension Host this.remoteCom = create((msg) => { if (logCommunication) { 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 - this.extensionHostProcessManager.startExtensionHostProcess((msg) => { + extensionHostMessagingProtocol.onMessage((msg) => { if (logCommunication) { 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 }); this.remoteCom.setManyHandler(this); - - lifecycleService.onShutdown(() => this.dispose()); - } - - public dispose(): void { - this.extensionHostProcessManager.terminate(); } protected _callOnRemote(proxyId: string, path: string, args: any[]): TPromise { return this.remoteCom.callOnRemote(proxyId, path, args); } -} - -class ExtensionHostProcessManager { - private initializeExtensionHostProcess: TPromise; - 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 { - return new TPromise((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 { - if (typeof extensionHostPort !== 'number') { - return TPromise.wrap(void 0); - } - return new TPromise((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 && (msg).type === '__$console') { - this.logExtensionHostMessage(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 { - 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