extensionHost.ts 17.7 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 30
import { generateRandomPipeName, Protocol } from 'vs/base/parts/ipc/node/ipc.net';
import { createServer, Server } from 'net';
J
Joao Moreno 已提交
31 32
import Event, { Emitter, debounceEvent, mapEvent, any } from 'vs/base/common/event';
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
export class LazyMessagePassingProtol implements IMessagePassingProtocol {

	private _delegate: IMessagePassingProtocol;
	private _onMessage = new Emitter<any>();
	private _buffer: any[] = [];

	readonly onMessage: Event<any> = this._onMessage.event;

	send(msg: any): void {
		if (this._delegate) {
			this._delegate.send(msg);
		} else {
			this._buffer.push(msg);
		}
	}

	resolve(delegate: IMessagePassingProtocol): void {
		this._delegate = delegate;
		this._delegate.onMessage(data => this._onMessage.fire(data));
		this._buffer.forEach(this._delegate.send, this._delegate);
		this._buffer = null;
	}
}

65
export class ExtensionHostProcessWorker {
66 67

	private extensionHostProcess: ChildProcess;
68 69 70 71

	private lastExtensionHostError: string;
	private terminating: boolean;

A
Alex Dima 已提交
72 73 74 75
	private readonly _isExtensionDevHost: boolean;
	private readonly _isExtensionDevTestFromCli: boolean;
	private readonly _isExtensionDevDebug: boolean;
	private readonly _isExtensionDevDebugBrk: boolean;
76

77
	readonly messagingProtocol = new LazyMessagePassingProtol();
78

79
	private extensionService: ExtensionService;
80

81
	constructor(
A
Alex Dima 已提交
82 83 84 85 86 87 88 89 90 91 92
		@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
93

94 95
	) {
		// handle extension host lifecycle a bit special when we know we are developing an extension that runs inside
A
Alex Dima 已提交
96 97 98 99
		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;
100

A
Alex Dima 已提交
101 102
		this._lifecycleService.onWillShutdown(this._onWillShutdown, this);
		this._lifecycleService.onShutdown(reason => this.terminate());
103

A
Alex Dima 已提交
104
		_broadcastService.onBroadcast(b => this.onBroadcast(b));
105 106 107 108 109
	}

	private onBroadcast(broadcast: IBroadcast): void {

		// Close Ext Host Window Request
A
Alex Dima 已提交
110
		if (broadcast.channel === EXTENSION_CLOSE_EXTHOST_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.closeWindow();
114 115
			}
		}
116

A
Alex Dima 已提交
117
		if (broadcast.channel === EXTENSION_RELOAD_BROADCAST_CHANNEL && this._isExtensionDevHost) {
118
			const extensionPaths = broadcast.payload as string[];
A
Alex Dima 已提交
119 120
			if (Array.isArray(extensionPaths) && extensionPaths.some(path => isEqual(this._environmentService.extensionDevelopmentPath, path, !isLinux))) {
				this._windowService.reloadWindow();
121 122
			}
		}
123 124
	}

125
	public start(extensionService: ExtensionService): TPromise<IMessagePassingProtocol> {
126
		this.extensionService = extensionService;
127

A
Alex Dima 已提交
128
		return TPromise.join<any>([this._tryListenOnPipe(), this._tryFindDebugPort()]).then(data => {
129 130 131
			const [server, hook] = <[Server, string]>data[0];
			const port = <number>data[1];

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

A
Alex Dima 已提交
152
			const crashReporterOptions = this._crashReporterService.getChildProcessStartOptions('extensionHost');
153 154 155 156
			if (crashReporterOptions) {
				opts.env.CRASH_REPORTER_START_OPTIONS = JSON.stringify(crashReporterOptions);
			}

157 158
			// Run Extension Host as fork of current process
			this.extensionHostProcess = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts);
159

160
			// Catch all output coming from the extension host process
J
Joao Moreno 已提交
161 162 163 164 165 166 167 168 169 170
			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'] }))
			);

171
			// Debounce all output, so we can render it in the Chrome console as a group
J
Joao Moreno 已提交
172 173 174 175
			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 };
176
			}, 100);
J
Joao Moreno 已提交
177

178
			// Print out extension host output
J
Joao Moreno 已提交
179 180 181 182 183 184
			onDebouncedOutput(data => {
				console.group('Extension Host');
				console.log(data.data, ...data.format);
				console.groupEnd();
			});

185 186 187
			// Support logging from extension host
			this.extensionHostProcess.on('message', msg => {
				if (msg && (<ILogEntry>msg).type === '__$console') {
A
Alex Dima 已提交
188
					this._logExtensionHostMessage(<ILogEntry>msg);
189 190 191 192
				}
			});

			// Lifecycle
A
Alex Dima 已提交
193 194 195 196 197 198 199
			const globalExitListener = () => this.terminate();
			process.once('exit', globalExitListener);
			this.extensionHostProcess.on('error', (err) => this._onError(err));
			this.extensionHostProcess.on('exit', (code: number, signal: string) => {
				process.removeListener('exit', globalExitListener);
				this._onExit(code, signal);
			});
200

201
			// Notify debugger that we are ready to attach to the process if we run a development extension
A
Alex Dima 已提交
202 203
			if (this._isExtensionDevHost && port) {
				this._broadcastService.broadcast({
204
					channel: EXTENSION_ATTACH_BROADCAST_CHANNEL,
205
					payload: {
A
Alex Dima 已提交
206
						debugId: this._environmentService.debugExtensionHost.debugId,
207 208
						port
					}
B
Benjamin Pasero 已提交
209
				});
210
			}
211

212 213
			// Help in case we fail to start it
			let startupTimeoutHandle: number;
A
Alex Dima 已提交
214
			if (!this._environmentService.isBuilt || this._isExtensionDevHost) {
215
				startupTimeoutHandle = setTimeout(() => {
A
Alex Dima 已提交
216
					const msg = this._isExtensionDevDebugBrk
217 218 219
						? 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 已提交
220
					this._messageService.show(Severity.Warning, msg);
221 222
				}, 10000);
			}
223

224
			// Initialize extension host process with hand shakes
A
Alex Dima 已提交
225
			return this._tryExtHostHandshake(server).then((protocol) => {
226 227 228
				clearTimeout(startupTimeoutHandle);
				return protocol;
			});
229 230
		});
	}
231

A
Alex Dima 已提交
232
	private _tryListenOnPipe(): TPromise<[Server, string]> {
233 234 235 236 237 238 239
		return new TPromise<[Server, string]>((resolve, reject) => {
			const server = createServer();
			server.on('error', reject);
			const hook = generateRandomPipeName();
			server.listen(hook, () => {
				server.removeListener('error', reject);
				resolve([server, hook]);
240
			});
241
		});
242 243
	}

A
Alex Dima 已提交
244 245
	private _tryFindDebugPort(): TPromise<number> {
		const extensionHostPort = this._environmentService.debugExtensionHost.port;
246
		if (typeof extensionHostPort !== 'number') {
247
			return TPromise.wrap<number>(void 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(server: Server): TPromise<IMessagePassingProtocol> {
269

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

272
			let handle = setTimeout(() => reject('timeout'), 60 * 1000);
273 274 275 276 277
			server.on('connection', socket => {
				clearTimeout(handle);
				const protocol = new Protocol(socket);
				resolve(protocol);
			});
278

279
		}).then(protocol => {
280

281 282 283 284
			return new TPromise<IMessagePassingProtocol>((resolve, reject) => {
				protocol.onMessage(msg => {
					if (msg === 'ready') {
						// 1) Host is ready to receive messages, initialize it
A
Alex Dima 已提交
285
						return this._createExtHostInitData().then(data => protocol.send(stringify(data)));
286 287 288 289 290 291 292
					} else if (msg === 'initialized') {
						// 2) Host is initialized
						this.messagingProtocol.resolve(protocol);
						resolve(protocol);
					}
					return undefined;
				});
293
			});
A
Alex Dima 已提交
294

295 296 297
		});
	}

A
Alex Dima 已提交
298 299 300
	private _createExtHostInitData(): TPromise<IInitData> {
		return TPromise.join<any>([this._telemetryService.getTelemetryInfo(), this.extensionService.getExtensions()]).then(([telemetryInfo, extensionDescriptions]) => {
			const r: IInitData = {
301 302
				parentPid: process.pid,
				environment: {
A
Alex Dima 已提交
303 304 305 306 307 308
					isExtensionDevelopmentDebug: this._isExtensionDevDebug,
					appSettingsHome: this._environmentService.appSettingsHome,
					disableExtensions: this._environmentService.disableExtensions,
					userExtensionsHome: this._environmentService.extensionsPath,
					extensionDevelopmentPath: this._environmentService.extensionDevelopmentPath,
					extensionTestsPath: this._environmentService.extensionTestsPath,
309
					// globally disable proposed api when built and not insiders developing extensions
A
Alex Dima 已提交
310 311
					enableProposedApiForAll: !this._environmentService.isBuilt || (!!this._environmentService.extensionDevelopmentPath && product.nameLong.indexOf('Insiders') >= 0),
					enableProposedApiFor: this._environmentService.args['enable-proposed-api'] || []
312
				},
A
Alex Dima 已提交
313
				workspace: <IWorkspaceData>this._contextService.getWorkspace(),
314
				extensions: extensionDescriptions,
A
Alex Dima 已提交
315
				configuration: this._configurationService.getConfigurationData(),
316
				telemetryInfo
317
			};
318
			return r;
319 320 321
		});
	}

A
Alex Dima 已提交
322
	private _logExtensionHostMessage(logEntry: ILogEntry) {
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
		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 已提交
342
		if (!this._isExtensionDevTestFromCli) {
343 344 345 346
			console[logEntry.severity].apply(console, consoleArgs);
		}

		// Log on main side if running tests from cli
A
Alex Dima 已提交
347 348
		if (this._isExtensionDevTestFromCli) {
			this._windowsService.log(logEntry.severity, ...args);
349 350 351
		}

		// Broadcast to other windows if we are in development mode
A
Alex Dima 已提交
352 353
		else if (!this._environmentService.isBuilt || this._isExtensionDevHost) {
			this._broadcastService.broadcast({
354
				channel: EXTENSION_LOG_BROADCAST_CHANNEL,
355 356
				payload: {
					logEntry,
A
Alex Dima 已提交
357
					debugId: this._environmentService.debugExtensionHost.debugId
358
				}
B
Benjamin Pasero 已提交
359
			});
360 361 362
		}
	}

A
Alex Dima 已提交
363
	private _onError(err: any): void {
364 365 366 367 368 369 370
		let errorMessage = toErrorMessage(err);
		if (errorMessage === this.lastExtensionHostError) {
			return; // prevent error spam
		}

		this.lastExtensionHostError = errorMessage;

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

A
Alex Dima 已提交
374 375 376 377 378
	private _onExit(code: number, signal: string): void {
		if (this.terminating) {
			// Expected termination path (we asked it to terminate)
			return;
		}
379

A
Alex Dima 已提交
380 381 382 383 384 385
		// 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;
			});
386

A
Alex Dima 已提交
387 388 389
			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.");
390
			}
A
Alex Dima 已提交
391 392 393 394 395 396 397
			this._messageService.show(Severity.Error, {
				message: message,
				actions: [
					openDevTools,
					this._instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, ReloadWindowAction.LABEL)
				]
			});
398

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

A
Alex Dima 已提交
402 403 404 405 406 407 408 409
		// 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);
410 411 412 413 414
		}
	}

	public terminate(): void {
		this.terminating = true;
415 416
		if (this.extensionHostProcess) {
			this.messagingProtocol.send({
417 418 419 420 421 422 423 424 425
				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
A
Alex Dima 已提交
426 427
		if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug) {
			this._broadcastService.broadcast({
428
				channel: EXTENSION_TERMINATE_BROADCAST_CHANNEL,
429
				payload: {
A
Alex Dima 已提交
430
					debugId: this._environmentService.debugExtensionHost.debugId
431
				}
B
Benjamin Pasero 已提交
432
			});
433 434 435 436

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