extensionHost.ts 19.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13
/*---------------------------------------------------------------------------------------------
 *  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 { 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';
14
import { Action } from 'vs/base/common/actions';
15
import { isWindows, isLinux } from 'vs/base/common/platform';
16 17 18
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';
19
import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows';
20
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
21
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
22 23
import { ChildProcess, fork } from 'child_process';
import { ipcRenderer as ipc } from 'electron';
24
import product from 'vs/platform/node/product';
25 26 27 28
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 { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
29
import { generateRandomPipeName, Protocol } from 'vs/base/parts/ipc/node/ipc.net';
A
Alex Dima 已提交
30 31
import { createServer, Server, Socket } from 'net';
import { debounceEvent, mapEvent, any } from 'vs/base/common/event';
J
Joao Moreno 已提交
32
import { fromEventEmitter } from 'vs/base/node/event';
33
import { IInitData, IWorkspaceData } from 'vs/workbench/api/node/extHost.protocol';
34
import { ExtensionService } from "vs/workbench/services/extensions/electron-browser/extensionService";
35
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
36
import { ICrashReporterService } from 'vs/workbench/services/crashReporter/common/crashReporterService';
37 38
import { IBroadcastService, IBroadcast } from "vs/platform/broadcast/electron-browser/broadcastService";
import { isEqual } from "vs/base/common/paths";
39
import { EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, ILogEntry, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from "vs/platform/extensions/common/extensionHost";
40 41

export class ExtensionHostProcessWorker {
42

A
Alex Dima 已提交
43 44 45
	private readonly _isExtensionDevHost: boolean;
	private readonly _isExtensionDevDebug: boolean;
	private readonly _isExtensionDevDebugBrk: boolean;
A
Alex Dima 已提交
46 47 48 49 50
	private readonly _isExtensionDevTestFromCli: boolean;

	// State
	private _lastExtensionHostError: string;
	private _terminating: boolean;
51

A
Alex Dima 已提交
52 53 54 55
	// Resources, in order they get acquired/created:
	private _namedPipeServer: Server;
	private _extensionHostProcess: ChildProcess;
	private _extensionHostConnection: Socket;
56
	private _messageProtocol: TPromise<IMessagePassingProtocol>;
57

58
	private extensionService: ExtensionService;
59

60
	constructor(
A
Alex Dima 已提交
61 62 63 64 65 66 67 68 69 70 71
		@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
		@IMessageService private readonly _messageService: IMessageService,
		@IWindowsService private readonly _windowsService: IWindowsService,
		@IWindowService private readonly _windowService: IWindowService,
		@IBroadcastService private readonly _broadcastService: IBroadcastService,
		@ILifecycleService private readonly _lifecycleService: ILifecycleService,
		@IInstantiationService private readonly _instantiationService: IInstantiationService,
		@IEnvironmentService private readonly _environmentService: IEnvironmentService,
		@IWorkspaceConfigurationService private readonly _configurationService: IWorkspaceConfigurationService,
		@ITelemetryService private readonly _telemetryService: ITelemetryService,
		@ICrashReporterService private readonly _crashReporterService: ICrashReporterService
72

73 74
	) {
		// handle extension host lifecycle a bit special when we know we are developing an extension that runs inside
A
Alex Dima 已提交
75 76 77 78
		this._isExtensionDevHost = this._environmentService.isExtensionDevelopment;
		this._isExtensionDevDebug = (typeof this._environmentService.debugExtensionHost.port === 'number');
		this._isExtensionDevDebugBrk = !!this._environmentService.debugExtensionHost.break;
		this._isExtensionDevTestFromCli = this._isExtensionDevHost && !!this._environmentService.extensionTestsPath && !this._environmentService.debugExtensionHost.break;
79

A
Alex Dima 已提交
80 81 82 83 84
		this._lastExtensionHostError = null;
		this._terminating = false;

		this._namedPipeServer = null;
		this._extensionHostProcess = null;
85 86
		this._extensionHostConnection = null;
		this._messageProtocol = null;
A
Alex Dima 已提交
87 88 89


		// TODO@rehost
A
Alex Dima 已提交
90
		this._lifecycleService.onWillShutdown(this._onWillShutdown, this);
A
Alex Dima 已提交
91 92

		// TODO@rehost
A
Alex Dima 已提交
93
		this._lifecycleService.onShutdown(reason => this.terminate());
94

A
Alex Dima 已提交
95 96
		// TODO@rehost
		_broadcastService.onBroadcast(b => this._onBroadcast(b));
97 98
	}

A
Alex Dima 已提交
99
	private _onBroadcast(broadcast: IBroadcast): void {
100 101

		// Close Ext Host Window Request
A
Alex Dima 已提交
102
		if (broadcast.channel === EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL && this._isExtensionDevHost) {
103
			const extensionPaths = broadcast.payload as string[];
A
Alex Dima 已提交
104 105
			if (Array.isArray(extensionPaths) && extensionPaths.some(path => isEqual(this._environmentService.extensionDevelopmentPath, path, !isLinux))) {
				this._windowService.closeWindow();
106 107
			}
		}
108

A
Alex Dima 已提交
109
		if (broadcast.channel === EXTENSION_RELOAD_BROADCAST_CHANNEL && this._isExtensionDevHost) {
110
			const extensionPaths = broadcast.payload as string[];
A
Alex Dima 已提交
111 112
			if (Array.isArray(extensionPaths) && extensionPaths.some(path => isEqual(this._environmentService.extensionDevelopmentPath, path, !isLinux))) {
				this._windowService.reloadWindow();
113 114
			}
		}
115 116
	}

117
	public start(extensionService: ExtensionService): TPromise<IMessagePassingProtocol> {
118 119 120 121
		if (this._terminating) {
			// .terminate() was called
			return null;
		}
J
Joao Moreno 已提交
122

123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
		if (!this._messageProtocol) {
			this.extensionService = extensionService;

			this._messageProtocol = TPromise.join<any>([this._tryListenOnPipe(), this._tryFindDebugPort()]).then((data: [string, number]) => {
				const pipeName = data[0];
				// The port will be 0 if there's no need to debug or if a free port was not found
				const port = data[1];

				const opts = {
					env: objects.mixin(objects.clone(process.env), {
						AMD_ENTRYPOINT: 'vs/workbench/node/extensionHostProcess',
						PIPE_LOGGING: 'true',
						VERBOSE_LOGGING: true,
						VSCODE_WINDOW_ID: String(this._windowService.getCurrentWindowId()),
						VSCODE_IPC_HOOK_EXTHOST: pipeName,
						ELECTRON_NO_ASAR: '1'
					}),
					// We only detach the extension host on windows. Linux and Mac orphan by default
					// and detach under Linux and Mac create another process group.
					// We detach because we have noticed that when the renderer exits, its child processes
					// (i.e. extension host) are taken down in a brutal fashion by the OS
					detached: !!isWindows,
					execArgv: port
						? ['--nolazy', (this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + port]
						: undefined,
					silent: true
				};

				const crashReporterOptions = this._crashReporterService.getChildProcessStartOptions('extensionHost');
				if (crashReporterOptions) {
					opts.env.CRASH_REPORTER_START_OPTIONS = JSON.stringify(crashReporterOptions);
154 155
				}

156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
				// Run Extension Host as fork of current process
				this._extensionHostProcess = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts);

				// Catch all output coming from the extension host process
				type Output = { data: string, format: string[] };
				this._extensionHostProcess.stdout.setEncoding('utf8');
				this._extensionHostProcess.stderr.setEncoding('utf8');
				const onStdout = fromEventEmitter<string>(this._extensionHostProcess.stdout, 'data');
				const onStderr = fromEventEmitter<string>(this._extensionHostProcess.stderr, 'data');
				const onOutput = any(
					mapEvent(onStdout, o => ({ data: `%c${o}`, format: [''] })),
					mapEvent(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }))
				);

				// Debounce all output, so we can render it in the Chrome console as a group
				const onDebouncedOutput = debounceEvent<Output>(onOutput, (r, o) => {
					return r
						? { data: r.data + o.data, format: [...r.format, ...o.format] }
						: { data: o.data, format: o.format };
				}, 100);

				// Print out extension host output
				onDebouncedOutput(data => {
					console.group('Extension Host');
					console.log(data.data, ...data.format);
					console.groupEnd();
				});
183

184 185 186 187
				// Support logging from extension host
				this._extensionHostProcess.on('message', msg => {
					if (msg && (<ILogEntry>msg).type === '__$console') {
						this._logExtensionHostMessage(<ILogEntry>msg);
188
					}
B
Benjamin Pasero 已提交
189
				});
190

191 192 193 194 195 196 197 198
				// Lifecycle
				const globalExitListener = () => this.terminate();
				process.once('exit', globalExitListener);
				this._extensionHostProcess.on('error', (err) => this._onExtHostProcessError(err));
				this._extensionHostProcess.on('exit', (code: number, signal: string) => {
					process.removeListener('exit', globalExitListener);
					this._onExtHostProcessExit(code, signal);
				});
199

200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
				// Notify debugger that we are ready to attach to the process if we run a development extension
				if (this._isExtensionDevHost && port) {
					this._broadcastService.broadcast({
						channel: EXTENSION_ATTACH_BROADCAST_CHANNEL,
						payload: {
							debugId: this._environmentService.debugExtensionHost.debugId,
							port
						}
					});
				}

				// Help in case we fail to start it
				let startupTimeoutHandle: number;
				if (!this._environmentService.isBuilt || this._isExtensionDevHost) {
					startupTimeoutHandle = setTimeout(() => {
						const msg = this._isExtensionDevDebugBrk
							? nls.localize('extensionHostProcess.startupFailDebug', "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.")
							: nls.localize('extensionHostProcess.startupFail', "Extension host did not start in 10 seconds, that might be a problem.");

						this._messageService.show(Severity.Warning, msg);
					}, 10000);
				}
222

223 224 225 226 227
				// Initialize extension host process with hand shakes
				return this._tryExtHostHandshake().then((protocol) => {
					clearTimeout(startupTimeoutHandle);
					return protocol;
				});
228
			});
229 230 231
		}

		return this._messageProtocol;
232
	}
233

A
Alex Dima 已提交
234
	/**
235
	 * Start a server (`this._namedPipeServer`) that listens on a named pipe and return the named pipe name.
A
Alex Dima 已提交
236
	 */
237 238 239
	private _tryListenOnPipe(): TPromise<string> {
		return new TPromise<string>((resolve, reject) => {
			const pipeName = generateRandomPipeName();
A
Alex Dima 已提交
240 241 242

			this._namedPipeServer = createServer();
			this._namedPipeServer.on('error', reject);
243
			this._namedPipeServer.listen(pipeName, () => {
A
Alex Dima 已提交
244
				this._namedPipeServer.removeListener('error', reject);
245
				resolve(pipeName);
246
			});
247
		});
248 249
	}

A
Alex Dima 已提交
250 251 252
	/**
	 * Find a free port if extension host debugging is enabled.
	 */
A
Alex Dima 已提交
253 254
	private _tryFindDebugPort(): TPromise<number> {
		const extensionHostPort = this._environmentService.debugExtensionHost.port;
255
		if (typeof extensionHostPort !== 'number') {
A
Alex Dima 已提交
256
			return TPromise.wrap<number>(0);
257 258 259 260 261
		}
		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');
B
Benjamin Pasero 已提交
262
					return c(void 0);
263 264
				}
				if (port !== extensionHostPort) {
B
Benjamin Pasero 已提交
265
					console.warn(`%c[Extension Host] %cProvided debugging port ${extensionHostPort} is not free, using ${port} instead.`, 'color: blue', 'color: black');
266
				}
A
Alex Dima 已提交
267
				if (this._isExtensionDevDebugBrk) {
B
Benjamin Pasero 已提交
268
					console.warn(`%c[Extension Host] %cSTOPPED on first line for debugging on port ${port}`, 'color: blue', 'color: black');
269
				} else {
B
Benjamin Pasero 已提交
270
					console.info(`%c[Extension Host] %cdebugger listening on port ${port}`, 'color: blue', 'color: black');
271 272 273 274 275 276
				}
				return c(port);
			});
		});
	}

277
	private _tryExtHostHandshake(): TPromise<IMessagePassingProtocol> {
278

279
		return new TPromise<IMessagePassingProtocol>((resolve, reject) => {
A
Alex Dima 已提交
280

A
Alex Dima 已提交
281 282
			// Wait for the extension host to connect to our named pipe
			// and wrap the socket in the message passing protocol
283 284 285 286 287 288
			let handle = setTimeout(() => {
				this._namedPipeServer.close();
				this._namedPipeServer = null;
				reject('timeout');
			}, 60 * 1000);

A
Alex Dima 已提交
289
			this._namedPipeServer.on('connection', socket => {
290
				clearTimeout(handle);
291 292
				this._namedPipeServer.close();
				this._namedPipeServer = null;
A
Alex Dima 已提交
293
				this._extensionHostConnection = socket;
294
				resolve(new Protocol(this._extensionHostConnection));
295
			});
296

297
		}).then((protocol) => {
A
Alex Dima 已提交
298 299 300

			// 1) wait for the incoming `ready` event and send the initialization data.
			// 2) wait for the incoming `initialized` event.
301
			return new TPromise<IMessagePassingProtocol>((resolve, reject) => {
A
Alex Dima 已提交
302

303 304 305 306 307
				let handle = setTimeout(() => {
					reject('timeout');
				}, 60 * 1000);

				const disposable = protocol.onMessage(msg => {
308

309
					if (msg === 'ready') {
310 311
						// 1) Extension Host is ready to receive messages, initialize it
						this._createExtHostInitData().then(data => protocol.send(stringify(data)));
A
Alex Dima 已提交
312 313 314 315
						return;
					}

					if (msg === 'initialized') {
316 317 318
						// 2) Extension Host is initialized

						clearTimeout(handle);
A
Alex Dima 已提交
319 320 321 322 323

						// stop listening for messages here
						disposable.dispose();

						// release this promise
324
						resolve(protocol);
A
Alex Dima 已提交
325
						return;
326
					}
A
Alex Dima 已提交
327 328

					console.error(`received unexpected message during handshake phase from the extension host: `, msg);
329
				});
A
Alex Dima 已提交
330

331
			});
A
Alex Dima 已提交
332

333 334 335
		});
	}

A
Alex Dima 已提交
336 337 338
	private _createExtHostInitData(): TPromise<IInitData> {
		return TPromise.join<any>([this._telemetryService.getTelemetryInfo(), this.extensionService.getExtensions()]).then(([telemetryInfo, extensionDescriptions]) => {
			const r: IInitData = {
339 340
				parentPid: process.pid,
				environment: {
A
Alex Dima 已提交
341 342 343 344 345 346
					isExtensionDevelopmentDebug: this._isExtensionDevDebug,
					appSettingsHome: this._environmentService.appSettingsHome,
					disableExtensions: this._environmentService.disableExtensions,
					userExtensionsHome: this._environmentService.extensionsPath,
					extensionDevelopmentPath: this._environmentService.extensionDevelopmentPath,
					extensionTestsPath: this._environmentService.extensionTestsPath,
347
					// globally disable proposed api when built and not insiders developing extensions
A
Alex Dima 已提交
348 349
					enableProposedApiForAll: !this._environmentService.isBuilt || (!!this._environmentService.extensionDevelopmentPath && product.nameLong.indexOf('Insiders') >= 0),
					enableProposedApiFor: this._environmentService.args['enable-proposed-api'] || []
350
				},
A
Alex Dima 已提交
351
				workspace: <IWorkspaceData>this._contextService.getWorkspace(),
352
				extensions: extensionDescriptions,
A
Alex Dima 已提交
353
				configuration: this._configurationService.getConfigurationData(),
354
				telemetryInfo
355
			};
356
			return r;
357 358 359
		});
	}

A
Alex Dima 已提交
360
	private _logExtensionHostMessage(logEntry: ILogEntry) {
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
		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
A
Alex Dima 已提交
380
		if (!this._isExtensionDevTestFromCli) {
381 382 383 384
			console[logEntry.severity].apply(console, consoleArgs);
		}

		// Log on main side if running tests from cli
A
Alex Dima 已提交
385 386
		if (this._isExtensionDevTestFromCli) {
			this._windowsService.log(logEntry.severity, ...args);
387 388 389
		}

		// Broadcast to other windows if we are in development mode
A
Alex Dima 已提交
390 391
		else if (!this._environmentService.isBuilt || this._isExtensionDevHost) {
			this._broadcastService.broadcast({
392
				channel: EXTENSION_LOG_BROADCAST_CHANNEL,
393 394
				payload: {
					logEntry,
A
Alex Dima 已提交
395
					debugId: this._environmentService.debugExtensionHost.debugId
396
				}
B
Benjamin Pasero 已提交
397
			});
398 399 400
		}
	}

A
Alex Dima 已提交
401
	private _onExtHostProcessError(err: any): void {
402
		let errorMessage = toErrorMessage(err);
A
Alex Dima 已提交
403
		if (errorMessage === this._lastExtensionHostError) {
404 405 406
			return; // prevent error spam
		}

A
Alex Dima 已提交
407
		this._lastExtensionHostError = errorMessage;
408

A
Alex Dima 已提交
409
		this._messageService.show(Severity.Error, nls.localize('extensionHostProcess.error', "Error from the extension host: {0}", errorMessage));
410 411
	}

A
Alex Dima 已提交
412 413 414
	private _onExtHostProcessExit(code: number, signal: string): void {
		if (this._terminating) {
			// Expected termination path (we asked the process to terminate)
A
Alex Dima 已提交
415 416
			return;
		}
417

A
Alex Dima 已提交
418 419 420 421 422 423
		// Unexpected termination
		if (!this._isExtensionDevHost) {
			const openDevTools = new Action('openDevTools', nls.localize('devTools', "Developer Tools"), '', true, async (): TPromise<boolean> => {
				await this._windowService.openDevTools();
				return false;
			});
424

A
Alex Dima 已提交
425 426 427
			let message = nls.localize('extensionHostProcess.crash', "Extension host terminated unexpectedly. Please reload the window to recover.");
			if (code === 87) {
				message = nls.localize('extensionHostProcess.unresponsiveCrash', "Extension host terminated because it was not responsive. Please reload the window to recover.");
428
			}
A
Alex Dima 已提交
429 430 431 432 433 434 435
			this._messageService.show(Severity.Error, {
				message: message,
				actions: [
					openDevTools,
					this._instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, ReloadWindowAction.LABEL)
				]
			});
436

A
Alex Dima 已提交
437 438
			console.error('Extension host terminated unexpectedly. Code: ', code, ' Signal: ', signal);
		}
439

A
Alex Dima 已提交
440 441 442 443 444 445 446 447
		// Expected development extension termination: When the extension host goes down we also shutdown the window
		else if (!this._isExtensionDevTestFromCli) {
			this._windowService.closeWindow();
		}

		// When CLI testing make sure to exit with proper exit code
		else {
			ipc.send('vscode:exit', code);
448 449 450 451
		}
	}

	public terminate(): void {
452 453 454
		if (this._terminating) {
			return;
		}
A
Alex Dima 已提交
455 456
		this._terminating = true;

457 458 459 460 461 462 463
		if (!this._messageProtocol) {
			// .start() was not called
			return;
		}

		this._messageProtocol.then((protocol) => {
			protocol.send({
464 465
				type: '__$terminate'
			});
466 467 468
		});

		// TODO@rehost: install a timer to clean up OS resources
469 470 471 472 473 474
	}

	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
A
Alex Dima 已提交
475 476
		if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug) {
			this._broadcastService.broadcast({
477
				channel: EXTENSION_TERMINATE_BROADCAST_CHANNEL,
478
				payload: {
A
Alex Dima 已提交
479
					debugId: this._environmentService.debugExtensionHost.debugId
480
				}
B
Benjamin Pasero 已提交
481
			});
482 483 484 485

			event.veto(TPromise.timeout(100 /* wait a bit for IPC to get delivered */).then(() => false));
		}
	}
486
}