extensionHostMain.ts 12.0 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

8 9
import * as nls from 'vs/nls';
import * as pfs from 'vs/base/node/pfs';
J
Johannes Rieken 已提交
10
import { TPromise } from 'vs/base/common/winjs.base';
11
import { join } from 'path';
J
Johannes Rieken 已提交
12
import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';
13 14
import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration';
import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
15
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
16 17
import { QueryType, ISearchQuery } from 'vs/platform/search/common/search';
import { DiskSearch } from 'vs/workbench/services/search/node/searchService';
18
import { IInitData, IEnvironment, IWorkspaceData, MainContext } from 'vs/workbench/api/node/extHost.protocol';
19
import * as errors from 'vs/base/common/errors';
20
import * as watchdog from 'native-watchdog';
A
Alex Dima 已提交
21
import * as glob from 'vs/base/common/glob';
A
Alex Dima 已提交
22
import { ExtensionActivatedByEvent } from 'vs/workbench/api/node/extHostExtensionActivator';
23
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
A
Alex Dima 已提交
24
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
A
Alex Dima 已提交
25
import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol';
26
import URI from 'vs/base/common/uri';
27
import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService';
E
Erich Gamma 已提交
28

29
// const nativeExit = process.exit.bind(process);
B
Benjamin Pasero 已提交
30
function patchProcess(allowExit: boolean) {
31
	process.exit = function (code) {
32 33 34 35 36 37
		if (allowExit) {
			exit(code);
		} else {
			const err = new Error('An extension called process.exit() and this was prevented.');
			console.warn(err.stack);
		}
38
	};
B
Benjamin Pasero 已提交
39 40 41 42 43

	process.crash = function () {
		const err = new Error('An extension called process.crash() and this was prevented.');
		console.warn(err.stack);
	};
44
}
45
export function exit(code?: number) {
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
	//nativeExit(code);

	// TODO@electron
	// See https://github.com/Microsoft/vscode/issues/32990
	// calling process.exit() does not exit the process when the process is being debugged
	// It waits for the debugger to disconnect, but in our version, the debugger does not
	// receive an event that the process desires to exit such that it can disconnect.

	// Do exactly what node.js would have done, minus the wait for the debugger part

	if (code || code === 0) {
		process.exitCode = code;
	}

	if (!(<any>process)._exiting) {
		(<any>process)._exiting = true;
		process.emit('exit', process.exitCode || 0);
	}
	watchdog.exit(process.exitCode || 0);
65 66
}

E
Erich Gamma 已提交
67
interface ITestRunner {
B
Benjamin Pasero 已提交
68
	run(testsRoot: string, clb: (error: Error, failures?: number) => void): void;
E
Erich Gamma 已提交
69 70
}

A
Alex Dima 已提交
71
export class ExtensionHostMain {
E
Erich Gamma 已提交
72

73
	private _isTerminating: boolean = false;
74
	private _diskSearch: DiskSearch;
75
	private _workspace: IWorkspaceData;
B
Benjamin Pasero 已提交
76
	private _environment: IEnvironment;
A
Alex Dima 已提交
77
	private _extensionService: ExtHostExtensionService;
78
	private _extHostConfiguration: ExtHostConfiguration;
79
	private _extHostLogService: ExtHostLogService;
80
	private disposables: IDisposable[] = [];
81

A
Alex Dima 已提交
82
	constructor(protocol: IMessagePassingProtocol, initData: IInitData) {
B
Benjamin Pasero 已提交
83
		this._environment = initData.environment;
84
		this._workspace = initData.workspace;
85

86
		const allowExit = !!this._environment.extensionTestsPath; // to support other test frameworks like Jasmin that use process.exit (https://github.com/Microsoft/vscode/issues/37708)
B
Benjamin Pasero 已提交
87
		patchProcess(allowExit);
88

89
		// services
A
Alex Dima 已提交
90
		const rpcProtocol = new RPCProtocol(protocol);
A
Alex Dima 已提交
91
		this._extHostLogService = new ExtHostLogService(initData.windowId, initData.logLevel, initData.logsPath);
92
		this.disposables.push(this._extHostLogService);
93
		const extHostWorkspace = new ExtHostWorkspace(rpcProtocol, initData.workspace, this._extHostLogService);
J
Joao Moreno 已提交
94

95 96
		this._extHostLogService.info('extension host started');
		this._extHostLogService.trace('initData', initData);
J
Joao Moreno 已提交
97

98
		this._extHostConfiguration = new ExtHostConfiguration(rpcProtocol.getProxy(MainContext.MainThreadConfiguration), extHostWorkspace, initData.configuration);
A
Alex Dima 已提交
99
		this._extensionService = new ExtHostExtensionService(initData, rpcProtocol, extHostWorkspace, this._extHostConfiguration, this._extHostLogService);
100

101
		// error forwarding and stack trace scanning
B
Benjamin Pasero 已提交
102
		Error.stackTraceLimit = 100; // increase number of stack frames (from 10, https://github.com/v8/v8/wiki/Stack-Trace-API)
103
		const extensionErrors = new WeakMap<Error, IExtensionDescription>();
104 105 106 107
		this._extensionService.getExtensionPathIndex().then(map => {
			(<any>Error).prepareStackTrace = (error: Error, stackTrace: errors.V8CallSite[]) => {
				let stackTraceMessage = '';
				let extension: IExtensionDescription;
J
Johannes Rieken 已提交
108
				let fileName: string;
109 110
				for (const call of stackTrace) {
					stackTraceMessage += `\n\tat ${call.toString()}`;
J
Johannes Rieken 已提交
111 112 113 114 115
					fileName = call.getFileName();
					if (!extension && fileName) {
						extension = map.findSubstr(fileName);
					}

116
				}
117 118
				extensionErrors.set(error, extension);
				return `${error.name || 'Error'}: ${error.message || ''}${stackTraceMessage}`;
119 120
			};
		});
121 122
		const mainThreadExtensions = rpcProtocol.getProxy(MainContext.MainThreadExtensionService);
		const mainThreadErrors = rpcProtocol.getProxy(MainContext.MainThreadErrors);
123 124
		errors.setUnexpectedErrorHandler(err => {
			const data = errors.transformErrorForSerialization(err);
125
			const extension = extensionErrors.get(err);
126 127 128 129 130
			if (extension) {
				mainThreadExtensions.$onExtensionRuntimeError(extension.id, data);
			} else {
				mainThreadErrors.$onUnexpectedError(data);
			}
131
		});
132 133

		// Configure the watchdog to kill our process if the JS event loop is unresponsive for more than 10s
A
Alex Dima 已提交
134 135 136
		// if (!initData.environment.isExtensionDevelopmentDebug) {
		// 	watchdog.start(10000);
		// }
137 138
	}

E
Erich Gamma 已提交
139
	public start(): TPromise<void> {
A
Alex Dima 已提交
140
		return this._extensionService.onExtensionAPIReady()
J
Johannes Rieken 已提交
141
			.then(() => this.handleEagerExtensions())
142 143
			.then(() => this.handleExtensionTests())
			.then(() => {
144
				this._extHostLogService.info(`eager extensions activated`);
145
			});
E
Erich Gamma 已提交
146 147
	}

148 149 150 151 152 153 154
	public terminate(): void {
		if (this._isTerminating) {
			// we are already shutting down...
			return;
		}
		this._isTerminating = true;

155 156
		this.disposables = dispose(this.disposables);

157 158 159 160 161
		errors.setUnexpectedErrorHandler((err) => {
			// TODO: write to log once we have one
		});

		let allPromises: TPromise<void>[] = [];
162
		try {
163
			let allExtensions = this._extensionService.getAllExtensionDescriptions();
164
			let allExtensionsIds = allExtensions.map(ext => ext.id);
A
Alex Dima 已提交
165
			let activatedExtensions = allExtensionsIds.filter(id => this._extensionService.isActivated(id));
166

167 168
			allPromises = activatedExtensions.map((extensionId) => {
				return this._extensionService.deactivate(extensionId);
169
			});
B
Benjamin Pasero 已提交
170
		} catch (err) {
171 172 173
			// TODO: write to log once we have one
		}

174 175
		let extensionsDeactivated = TPromise.join(allPromises).then<void>(() => void 0);

176 177
		// Give extensions 1 second to wrap up any async dispose, then exit
		setTimeout(() => {
178
			TPromise.any<void>([TPromise.timeout(4000), extensionsDeactivated]).then(() => exit(), () => exit());
179 180 181
		}, 1000);
	}

A
Alex Dima 已提交
182 183
	// Handle "eager" activation extensions
	private handleEagerExtensions(): TPromise<void> {
184
		this._extensionService.activateByEvent('*', true).then(null, (err) => {
E
Erich Gamma 已提交
185 186
			console.error(err);
		});
A
Alex Dima 已提交
187
		return this.handleWorkspaceContainsEagerExtensions();
E
Erich Gamma 已提交
188 189
	}

A
Alex Dima 已提交
190
	private handleWorkspaceContainsEagerExtensions(): TPromise<void> {
S
Sandeep Somavarapu 已提交
191
		if (!this._workspace || this._workspace.folders.length === 0) {
E
Erich Gamma 已提交
192 193 194
			return TPromise.as(null);
		}

A
Alex Dima 已提交
195 196 197 198 199 200
		return TPromise.join(
			this._extensionService.getAllExtensionDescriptions().map((desc) => {
				return this.handleWorkspaceContainsEagerExtension(desc);
			})
		).then(() => { });
	}
E
Erich Gamma 已提交
201

A
Alex Dima 已提交
202 203 204 205 206
	private handleWorkspaceContainsEagerExtension(desc: IExtensionDescription): TPromise<void> {
		let activationEvents = desc.activationEvents;
		if (!activationEvents) {
			return TPromise.as(void 0);
		}
E
Erich Gamma 已提交
207

A
Alex Dima 已提交
208 209 210 211 212 213 214 215 216 217
		const fileNames: string[] = [];
		const globPatterns: string[] = [];

		for (let i = 0; i < activationEvents.length; i++) {
			if (/^workspaceContains:/.test(activationEvents[i])) {
				let fileNameOrGlob = activationEvents[i].substr('workspaceContains:'.length);
				if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) {
					globPatterns.push(fileNameOrGlob);
				} else {
					fileNames.push(fileNameOrGlob);
E
Erich Gamma 已提交
218 219
				}
			}
A
Alex Dima 已提交
220
		}
E
Erich Gamma 已提交
221

A
Alex Dima 已提交
222 223 224
		if (fileNames.length === 0 && globPatterns.length === 0) {
			return TPromise.as(void 0);
		}
225

A
Alex Dima 已提交
226 227 228 229 230 231 232 233 234 235
		let fileNamePromise = TPromise.join(fileNames.map((fileName) => this.activateIfFileName(desc.id, fileName))).then(() => { });
		let globPatternPromise = this.activateIfGlobPatterns(desc.id, globPatterns);

		return TPromise.join([fileNamePromise, globPatternPromise]).then(() => { });
	}

	private async activateIfFileName(extensionId: string, fileName: string): TPromise<void> {
		// find exact path

		for (const { uri } of this._workspace.folders) {
236
			if (await pfs.exists(join(URI.revive(uri).fsPath, fileName))) {
A
Alex Dima 已提交
237 238
				// the file was found
				return (
A
Alex Dima 已提交
239
					this._extensionService.activateById(extensionId, new ExtensionActivatedByEvent(true, `workspaceContains:${fileName}`))
A
Alex Dima 已提交
240 241
						.done(null, err => console.error(err))
				);
242
			}
A
Alex Dima 已提交
243 244 245 246
		}

		return undefined;
	}
J
Joao Moreno 已提交
247

A
Alex Dima 已提交
248
	private async activateIfGlobPatterns(extensionId: string, globPatterns: string[]): TPromise<void> {
249 250
		this._extHostLogService.trace(`extensionHostMain#activateIfGlobPatterns: fileSearch, extension: ${extensionId}, entryPoint: workspaceContains`);

251 252 253 254
		if (globPatterns.length === 0) {
			return TPromise.as(void 0);
		}

A
Alex Dima 已提交
255 256 257 258
		if (!this._diskSearch) {
			// Shut down this search process after 1s
			this._diskSearch = new DiskSearch(false, 1000);
		}
E
Erich Gamma 已提交
259

A
Alex Dima 已提交
260 261 262
		let includes: glob.IExpression = {};
		globPatterns.forEach((globPattern) => {
			includes[globPattern] = true;
E
Erich Gamma 已提交
263
		});
A
Alex Dima 已提交
264

265
		const folderQueries = this._workspace.folders.map(folder => ({ folder: URI.revive(folder.uri) }));
R
Rob Lourens 已提交
266 267 268
		const config = this._extHostConfiguration.getConfiguration('search');
		const useRipgrep = config.get('useRipgrep', true);
		const followSymlinks = config.get('followSymlinks', true);
269

A
Alex Dima 已提交
270
		const query: ISearchQuery = {
271
			folderQueries,
A
Alex Dima 已提交
272
			type: QueryType.File,
273
			exists: true,
274
			includePattern: includes,
275 276
			useRipgrep,
			ignoreSymlinks: !followSymlinks
A
Alex Dima 已提交
277 278 279
		};

		let result = await this._diskSearch.search(query);
280
		if (result.limitHit) {
A
Alex Dima 已提交
281 282
			// a file was found matching one of the glob patterns
			return (
A
Alex Dima 已提交
283
				this._extensionService.activateById(extensionId, new ExtensionActivatedByEvent(true, `workspaceContains:${globPatterns.join(',')}`))
A
Alex Dima 已提交
284 285 286 287 288
					.done(null, err => console.error(err))
			);
		}

		return TPromise.as(void 0);
E
Erich Gamma 已提交
289 290
	}

A
Alex Dima 已提交
291
	private handleExtensionTests(): TPromise<void> {
B
Benjamin Pasero 已提交
292
		if (!this._environment.extensionTestsPath || !this._environment.extensionDevelopmentPath) {
E
Erich Gamma 已提交
293 294 295 296
			return TPromise.as(null);
		}

		// Require the test runner via node require from the provided path
B
Benjamin Pasero 已提交
297 298
		let testRunner: ITestRunner;
		let requireError: Error;
E
Erich Gamma 已提交
299
		try {
B
Benjamin Pasero 已提交
300
			testRunner = <any>require.__$__nodeRequire(this._environment.extensionTestsPath);
E
Erich Gamma 已提交
301 302 303 304 305 306 307
		} catch (error) {
			requireError = error;
		}

		// Execute the runner if it follows our spec
		if (testRunner && typeof testRunner.run === 'function') {
			return new TPromise<void>((c, e) => {
B
Benjamin Pasero 已提交
308
				testRunner.run(this._environment.extensionTestsPath, (error, failures) => {
E
Erich Gamma 已提交
309 310 311 312 313 314
					if (error) {
						e(error.toString());
					} else {
						c(null);
					}

315
					// after tests have run, we shutdown the host
316
					this.gracefulExit(failures && failures > 0 ? 1 /* ERROR */ : 0 /* OK */);
E
Erich Gamma 已提交
317 318 319 320
				});
			});
		}

321 322
		// Otherwise make sure to shutdown anyway even in case of an error
		else {
323
			this.gracefulExit(1 /* ERROR */);
324
		}
E
Erich Gamma 已提交
325

326
		return TPromise.wrapError<void>(new Error(requireError ? requireError.toString() : nls.localize('extensionTestError', "Path {0} does not point to a valid extension test runner.", this._environment.extensionTestsPath)));
E
Erich Gamma 已提交
327
	}
328

329
	private gracefulExit(code: number): void {
330
		// to give the PH process a chance to flush any outstanding console
331
		// messages to the main process, we delay the exit() by some time
332
		setTimeout(() => exit(code), 500);
333
	}
334
}