extensionHost.ts 18.4 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 56 57
	// Resources, in order they get acquired/created:
	private _namedPipeServer: Server;
	private _namedPipeName: string;
	private _extensionHostProcess: ChildProcess;
	private _extensionHostConnection: Socket;
	private _messagingProtocol: IMessagePassingProtocol;
58

59
	private extensionService: ExtensionService;
60

61
	constructor(
A
Alex Dima 已提交
62 63 64 65 66 67 68 69 70 71 72
		@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
73

74 75
	) {
		// handle extension host lifecycle a bit special when we know we are developing an extension that runs inside
A
Alex Dima 已提交
76 77 78 79
		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;
80

A
Alex Dima 已提交
81 82 83 84 85 86 87 88 89 90
		this._lastExtensionHostError = null;
		this._terminating = false;

		this._namedPipeServer = null;
		this._namedPipeName = null;
		this._extensionHostProcess = null;
		this._messagingProtocol = null;


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

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

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

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

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

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

118
	public start(extensionService: ExtensionService): TPromise<IMessagePassingProtocol> {
119
		this.extensionService = extensionService;
120

A
Alex Dima 已提交
121 122 123
		return TPromise.join<any>([this._tryListenOnPipe(), this._tryFindDebugPort()]).then((data: [void, number]) => {
			// The port will be 0 if there's no need to debug or if a free port was not found
			const port = data[1];
124

125
			const opts = {
126 127 128 129
				env: objects.mixin(objects.clone(process.env), {
					AMD_ENTRYPOINT: 'vs/workbench/node/extensionHostProcess',
					PIPE_LOGGING: 'true',
					VERBOSE_LOGGING: true,
A
Alex Dima 已提交
130
					VSCODE_WINDOW_ID: String(this._windowService.getCurrentWindowId()),
A
Alex Dima 已提交
131
					VSCODE_IPC_HOOK_EXTHOST: this._namedPipeName,
132 133 134 135 136
					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
A
Alex Dima 已提交
137
				// (i.e. extension host) are taken down in a brutal fashion by the OS
138 139
				detached: !!isWindows,
				execArgv: port
A
Alex Dima 已提交
140
					? ['--nolazy', (this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + port]
J
Joao Moreno 已提交
141 142
					: undefined,
				silent: true
143
			};
144

A
Alex Dima 已提交
145
			const crashReporterOptions = this._crashReporterService.getChildProcessStartOptions('extensionHost');
146 147 148 149
			if (crashReporterOptions) {
				opts.env.CRASH_REPORTER_START_OPTIONS = JSON.stringify(crashReporterOptions);
			}

150
			// Run Extension Host as fork of current process
A
Alex Dima 已提交
151
			this._extensionHostProcess = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts);
152

153
			// Catch all output coming from the extension host process
J
Joao Moreno 已提交
154
			type Output = { data: string, format: string[] };
A
Alex Dima 已提交
155 156 157 158
			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');
J
Joao Moreno 已提交
159 160 161 162 163
			const onOutput = any(
				mapEvent(onStdout, o => ({ data: `%c${o}`, format: [''] })),
				mapEvent(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }))
			);

164
			// Debounce all output, so we can render it in the Chrome console as a group
J
Joao Moreno 已提交
165 166 167 168
			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 };
169
			}, 100);
J
Joao Moreno 已提交
170

171
			// Print out extension host output
J
Joao Moreno 已提交
172 173 174 175 176 177
			onDebouncedOutput(data => {
				console.group('Extension Host');
				console.log(data.data, ...data.format);
				console.groupEnd();
			});

178
			// Support logging from extension host
A
Alex Dima 已提交
179
			this._extensionHostProcess.on('message', msg => {
180
				if (msg && (<ILogEntry>msg).type === '__$console') {
A
Alex Dima 已提交
181
					this._logExtensionHostMessage(<ILogEntry>msg);
182 183 184 185
				}
			});

			// Lifecycle
A
Alex Dima 已提交
186 187
			const globalExitListener = () => this.terminate();
			process.once('exit', globalExitListener);
A
Alex Dima 已提交
188 189
			this._extensionHostProcess.on('error', (err) => this._onExtHostProcessError(err));
			this._extensionHostProcess.on('exit', (code: number, signal: string) => {
A
Alex Dima 已提交
190
				process.removeListener('exit', globalExitListener);
A
Alex Dima 已提交
191
				this._onExtHostProcessExit(code, signal);
A
Alex Dima 已提交
192
			});
193

194
			// Notify debugger that we are ready to attach to the process if we run a development extension
A
Alex Dima 已提交
195 196
			if (this._isExtensionDevHost && port) {
				this._broadcastService.broadcast({
197
					channel: EXTENSION_ATTACH_BROADCAST_CHANNEL,
198
					payload: {
A
Alex Dima 已提交
199
						debugId: this._environmentService.debugExtensionHost.debugId,
200 201
						port
					}
B
Benjamin Pasero 已提交
202
				});
203
			}
204

205 206
			// Help in case we fail to start it
			let startupTimeoutHandle: number;
A
Alex Dima 已提交
207
			if (!this._environmentService.isBuilt || this._isExtensionDevHost) {
208
				startupTimeoutHandle = setTimeout(() => {
A
Alex Dima 已提交
209
					const msg = this._isExtensionDevDebugBrk
210 211 212
						? 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.");

A
Alex Dima 已提交
213
					this._messageService.show(Severity.Warning, msg);
214 215
				}, 10000);
			}
216

217
			// Initialize extension host process with hand shakes
A
Alex Dima 已提交
218
			return this._tryExtHostHandshake().then((protocol) => {
219
				clearTimeout(startupTimeoutHandle);
A
Alex Dima 已提交
220
				return this._messagingProtocol;
221
			});
222 223
		});
	}
224

A
Alex Dima 已提交
225 226 227 228 229 230 231 232 233 234 235 236
	/**
	 * Start a server (`this._namedPipeServer`) that listens on a named pipe (`this._namedPipeName`)
	 */
	private _tryListenOnPipe(): TPromise<void> {
		return new TPromise<void>((resolve, reject) => {
			this._namedPipeName = generateRandomPipeName();

			this._namedPipeServer = createServer();
			this._namedPipeServer.on('error', reject);
			this._namedPipeServer.listen(this._namedPipeName, () => {
				this._namedPipeServer.removeListener('error', reject);
				resolve(void 0);
237
			});
238
		});
239 240
	}

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

A
Alex Dima 已提交
268
	private _tryExtHostHandshake(): TPromise<void> {
269

A
Alex Dima 已提交
270
		return new TPromise<void>((resolve, reject) => {
A
Alex Dima 已提交
271

A
Alex Dima 已提交
272 273
			// Wait for the extension host to connect to our named pipe
			// and wrap the socket in the message passing protocol
274
			let handle = setTimeout(() => reject('timeout'), 60 * 1000);
A
Alex Dima 已提交
275
			this._namedPipeServer.on('connection', socket => {
276
				clearTimeout(handle);
A
Alex Dima 已提交
277 278 279
				this._extensionHostConnection = socket;
				this._messagingProtocol = new Protocol(this._extensionHostConnection);
				resolve(void 0);
280
			});
281

A
Alex Dima 已提交
282 283 284 285 286 287 288
		}).then(() => {

			// 1) wait for the incoming `ready` event and send the initialization data.
			// 2) wait for the incoming `initialized` event.
			return new TPromise<void>((resolve, reject) => {

				const disposable = this._messagingProtocol.onMessage(msg => {
289

290 291
					if (msg === 'ready') {
						// 1) Host is ready to receive messages, initialize it
A
Alex Dima 已提交
292 293 294 295 296
						this._createExtHostInitData().then(data => this._messagingProtocol.send(stringify(data)));
						return;
					}

					if (msg === 'initialized') {
297
						// 2) Host is initialized
A
Alex Dima 已提交
298 299 300 301 302 303 304

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

						// release this promise
						resolve(void 0);
						return;
305
					}
A
Alex Dima 已提交
306 307

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

310
			});
A
Alex Dima 已提交
311

312 313 314
		});
	}

A
Alex Dima 已提交
315 316 317
	private _createExtHostInitData(): TPromise<IInitData> {
		return TPromise.join<any>([this._telemetryService.getTelemetryInfo(), this.extensionService.getExtensions()]).then(([telemetryInfo, extensionDescriptions]) => {
			const r: IInitData = {
318 319
				parentPid: process.pid,
				environment: {
A
Alex Dima 已提交
320 321 322 323 324 325
					isExtensionDevelopmentDebug: this._isExtensionDevDebug,
					appSettingsHome: this._environmentService.appSettingsHome,
					disableExtensions: this._environmentService.disableExtensions,
					userExtensionsHome: this._environmentService.extensionsPath,
					extensionDevelopmentPath: this._environmentService.extensionDevelopmentPath,
					extensionTestsPath: this._environmentService.extensionTestsPath,
326
					// globally disable proposed api when built and not insiders developing extensions
A
Alex Dima 已提交
327 328
					enableProposedApiForAll: !this._environmentService.isBuilt || (!!this._environmentService.extensionDevelopmentPath && product.nameLong.indexOf('Insiders') >= 0),
					enableProposedApiFor: this._environmentService.args['enable-proposed-api'] || []
329
				},
A
Alex Dima 已提交
330
				workspace: <IWorkspaceData>this._contextService.getWorkspace(),
331
				extensions: extensionDescriptions,
A
Alex Dima 已提交
332
				configuration: this._configurationService.getConfigurationData(),
333
				telemetryInfo
334
			};
335
			return r;
336 337 338
		});
	}

A
Alex Dima 已提交
339
	private _logExtensionHostMessage(logEntry: ILogEntry) {
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
		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 已提交
359
		if (!this._isExtensionDevTestFromCli) {
360 361 362 363
			console[logEntry.severity].apply(console, consoleArgs);
		}

		// Log on main side if running tests from cli
A
Alex Dima 已提交
364 365
		if (this._isExtensionDevTestFromCli) {
			this._windowsService.log(logEntry.severity, ...args);
366 367 368
		}

		// Broadcast to other windows if we are in development mode
A
Alex Dima 已提交
369 370
		else if (!this._environmentService.isBuilt || this._isExtensionDevHost) {
			this._broadcastService.broadcast({
371
				channel: EXTENSION_LOG_BROADCAST_CHANNEL,
372 373
				payload: {
					logEntry,
A
Alex Dima 已提交
374
					debugId: this._environmentService.debugExtensionHost.debugId
375
				}
B
Benjamin Pasero 已提交
376
			});
377 378 379
		}
	}

A
Alex Dima 已提交
380
	private _onExtHostProcessError(err: any): void {
381
		let errorMessage = toErrorMessage(err);
A
Alex Dima 已提交
382
		if (errorMessage === this._lastExtensionHostError) {
383 384 385
			return; // prevent error spam
		}

A
Alex Dima 已提交
386
		this._lastExtensionHostError = errorMessage;
387

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

A
Alex Dima 已提交
391 392 393
	private _onExtHostProcessExit(code: number, signal: string): void {
		if (this._terminating) {
			// Expected termination path (we asked the process to terminate)
A
Alex Dima 已提交
394 395
			return;
		}
396

A
Alex Dima 已提交
397 398 399 400 401 402
		// 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;
			});
403

A
Alex Dima 已提交
404 405 406
			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.");
407
			}
A
Alex Dima 已提交
408 409 410 411 412 413 414
			this._messageService.show(Severity.Error, {
				message: message,
				actions: [
					openDevTools,
					this._instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, ReloadWindowAction.LABEL)
				]
			});
415

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

A
Alex Dima 已提交
419 420 421 422 423 424 425 426
		// 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);
427 428 429 430
		}
	}

	public terminate(): void {
A
Alex Dima 已提交
431 432 433 434
		this._terminating = true;

		if (this._messagingProtocol) {
			this._messagingProtocol.send({
435 436
				type: '__$terminate'
			});
A
Alex Dima 已提交
437 438
		} else {
			// TODO@rehost
439 440 441 442 443 444 445
		}
	}

	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 已提交
446 447
		if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug) {
			this._broadcastService.broadcast({
448
				channel: EXTENSION_TERMINATE_BROADCAST_CHANNEL,
449
				payload: {
A
Alex Dima 已提交
450
					debugId: this._environmentService.debugExtensionHost.debugId
451
				}
B
Benjamin Pasero 已提交
452
			});
453 454 455 456

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