diff --git a/src/vs/workbench/services/thread/electron-browser/threadService.ts b/src/vs/workbench/services/thread/electron-browser/threadService.ts index f867f0854aec7..c9125eeaa5432 100644 --- a/src/vs/workbench/services/thread/electron-browser/threadService.ts +++ b/src/vs/workbench/services/thread/electron-browser/threadService.ts @@ -132,7 +132,13 @@ class ExtensionHostProcessManager { 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 @@ -145,20 +151,16 @@ class ExtensionHostProcessManager { } // Initialize extension host process with hand shakes - this.initializeExtensionHostProcess = new TPromise((c, e) => { + this.initializeExtensionHostProcess = this.doInitializeExtensionHostProcess(opts); + } + private doInitializeExtensionHostProcess(opts: any): TPromise { + return new TPromise((c, e) => { // Resolve additional execution args (e.g. debug) - return this.resolveDebugPort(this.environmentService.debugExtensionHost.port, port => { + this.resolveDebugPort(this.environmentService.debugExtensionHost.port).then(port => { if (port) { opts.execArgv = ['--nolazy', (this.isExtensionDevelopmentDebugging ? '--debug-brk=' : '--debug=') + port]; } - // We only detach the extension host on windows. Linux and Mac orphan by default - // and detach under Linux and Mac create another process group. - if (isWindows) { - // 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 - opts.detached = true; - } // Run Extension Host as fork of current process this.extensionHostProcessHandle = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts); @@ -172,159 +174,165 @@ class ExtensionHostProcessManager { } // Messages from Extension host - this.extensionHostProcessHandle.on('message', (msg) => { - - // 1) Host is ready to receive messages, initialize it - if (msg === 'ready') { - if (this.initializeTimer) { - window.clearTimeout(this.initializeTimer); - } - - 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() - } - }); - - this.extensionHostProcessHandle.send(initPayload); - } - - // 2) Host is initialized - else if (msg === 'initialized') { - this.unsentMessages.forEach(m => this.postMessage(m)); - this.unsentMessages = []; - - this.extensionHostProcessReady = true; + this.extensionHostProcessHandle.on('message', msg => { + if (this.onMessaage(msg, opts.onExtensionHostMessage)) { c(this.extensionHostProcessHandle); } - - // Support logging from extension host - else if (msg && (msg).type === '__$console') { - let logEntry: ILogEntry = msg; - - 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 */); - } - } - - // Any other message goes to the callback - else { - onExtensionHostMessage(msg); - } }); // Lifecycle let onExit = () => this.terminate(); process.once('exit', onExit); - - this.extensionHostProcessHandle.on('error', (err) => { - 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)); - }); - - this.extensionHostProcessHandle.on('exit', (code: any, signal: any) => { - process.removeListener('exit', onExit); - - 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); - } - } - }); + 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, clb: (port: number) => void): void { - - // Check for a free debugging port - if (typeof extensionHostPort === 'number') { - return findFreePort(extensionHostPort, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, (port) => { + 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'); - - return clb(void 0); + 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 clb(port); + 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); } - // Nothing to do here - else { - return clb(void 0); + 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() + } + }); + + this.extensionHostProcessHandle.send(initPayload); + } + + 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); + } } }