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

6
import * as nls from 'vs/nls';
7
import { join } from 'path';
A
Alex Dima 已提交
8 9
import { timeout } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
10
import * as errors from 'vs/base/common/errors';
11
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
A
Alex Dima 已提交
12
import { Counter } from 'vs/base/common/numbers';
13
import { URI, setUriThrowOnMissingScheme } from 'vs/base/common/uri';
A
Alex Dima 已提交
14
import { IURITransformer } from 'vs/base/common/uriIpc';
A
Alex Dima 已提交
15 16 17 18 19 20
import * as pfs from 'vs/base/node/pfs';
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
import { IEnvironment, IInitData, IWorkspaceData, MainContext, MainThreadWorkspaceShape } from 'vs/workbench/api/node/extHost.protocol';
import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration';
import { ExtensionActivatedByEvent } from 'vs/workbench/api/node/extHostExtensionActivator';
import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';
21
import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService';
A
Alex Dima 已提交
22 23 24
import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol';
E
Erich Gamma 已提交
25

26 27 28 29
// we don't (yet) throw when extensions parse
// uris that have no scheme
setUriThrowOnMissingScheme(false);

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

	process.crash = function () {
		const err = new Error('An extension called process.crash() and this was prevented.');
		console.warn(err.stack);
	};
45
}
B
Benjamin Pasero 已提交
46

47
export function exit(code?: number) {
48
	nativeExit(code);
49 50
}

E
Erich Gamma 已提交
51
interface ITestRunner {
B
Benjamin Pasero 已提交
52
	run(testsRoot: string, clb: (error: Error, failures?: number) => void): void;
E
Erich Gamma 已提交
53 54
}

A
Alex Dima 已提交
55
export class ExtensionHostMain {
E
Erich Gamma 已提交
56

57
	private static readonly WORKSPACE_CONTAINS_TIMEOUT = 7000;
58

59
	private _isTerminating: boolean = false;
60
	private _workspace: IWorkspaceData;
B
Benjamin Pasero 已提交
61
	private _environment: IEnvironment;
A
Alex Dima 已提交
62
	private _extensionService: ExtHostExtensionService;
63
	private _extHostConfiguration: ExtHostConfiguration;
64
	private _extHostLogService: ExtHostLogService;
65
	private disposables: IDisposable[] = [];
66

67 68 69
	private _searchRequestIdProvider: Counter;
	private _mainThreadWorkspace: MainThreadWorkspaceShape;

A
Alex Dima 已提交
70
	constructor(protocol: IMessagePassingProtocol, initData: IInitData) {
A
Alex Dima 已提交
71 72
		const uriTransformer: IURITransformer = null;
		const rpcProtocol = new RPCProtocol(protocol, null, uriTransformer);
73 74 75

		// ensure URIs are transformed and revived
		initData = this.transform(initData, rpcProtocol);
B
Benjamin Pasero 已提交
76
		this._environment = initData.environment;
77
		this._workspace = initData.workspace;
78

79
		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 已提交
80
		patchProcess(allowExit);
81

82
		// services
83
		this._extHostLogService = new ExtHostLogService(initData.logLevel, initData.logsLocation.fsPath);
84
		this.disposables.push(this._extHostLogService);
85 86 87

		this._searchRequestIdProvider = new Counter();
		const extHostWorkspace = new ExtHostWorkspace(rpcProtocol, initData.workspace, this._extHostLogService, this._searchRequestIdProvider);
J
Joao Moreno 已提交
88

89 90
		this._extHostLogService.info('extension host started');
		this._extHostLogService.trace('initData', initData);
J
Joao Moreno 已提交
91

92
		this._extHostConfiguration = new ExtHostConfiguration(rpcProtocol.getProxy(MainContext.MainThreadConfiguration), extHostWorkspace, initData.configuration);
93 94
		const mainThreadTelemetry = rpcProtocol.getProxy(MainContext.MainThreadTelemetry);
		this._extensionService = new ExtHostExtensionService(initData, rpcProtocol, extHostWorkspace, this._extHostConfiguration, this._extHostLogService, mainThreadTelemetry);
95

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

111
				}
112 113
				extensionErrors.set(error, extension);
				return `${error.name || 'Error'}: ${error.message || ''}${stackTraceMessage}`;
114 115
			};
		});
B
Benjamin Pasero 已提交
116

117 118
		const mainThreadExtensions = rpcProtocol.getProxy(MainContext.MainThreadExtensionService);
		const mainThreadErrors = rpcProtocol.getProxy(MainContext.MainThreadErrors);
119 120
		errors.setUnexpectedErrorHandler(err => {
			const data = errors.transformErrorForSerialization(err);
121
			const extension = extensionErrors.get(err);
122 123 124 125 126
			if (extension) {
				mainThreadExtensions.$onExtensionRuntimeError(extension.id, data);
			} else {
				mainThreadErrors.$onUnexpectedError(data);
			}
127
		});
128 129

		this._mainThreadWorkspace = rpcProtocol.getProxy(MainContext.MainThreadWorkspace);
130 131
	}

A
Alex Dima 已提交
132
	start(): Thenable<void> {
A
Alex Dima 已提交
133
		return this._extensionService.onExtensionAPIReady()
J
Johannes Rieken 已提交
134
			.then(() => this.handleEagerExtensions())
135 136
			.then(() => this.handleExtensionTests())
			.then(() => {
137
				this._extHostLogService.info(`eager extensions activated`);
138
			});
E
Erich Gamma 已提交
139 140
	}

B
Benjamin Pasero 已提交
141
	terminate(): void {
142 143 144 145 146 147
		if (this._isTerminating) {
			// we are already shutting down...
			return;
		}
		this._isTerminating = true;

148 149
		this.disposables = dispose(this.disposables);

150 151 152 153
		errors.setUnexpectedErrorHandler((err) => {
			// TODO: write to log once we have one
		});

A
Alex Dima 已提交
154
		let allPromises: Thenable<void>[] = [];
155
		try {
B
Benjamin Pasero 已提交
156 157 158
			const allExtensions = this._extensionService.getAllExtensionDescriptions();
			const allExtensionsIds = allExtensions.map(ext => ext.id);
			const activatedExtensions = allExtensionsIds.filter(id => this._extensionService.isActivated(id));
159

160 161
			allPromises = activatedExtensions.map((extensionId) => {
				return this._extensionService.deactivate(extensionId);
162
			});
B
Benjamin Pasero 已提交
163
		} catch (err) {
164 165 166
			// TODO: write to log once we have one
		}

A
Alex Dima 已提交
167
		const extensionsDeactivated = Promise.all(allPromises).then<void>(() => void 0);
168

169 170
		// Give extensions 1 second to wrap up any async dispose, then exit
		setTimeout(() => {
171
			Promise.race([timeout(4000), extensionsDeactivated]).then(() => exit(), () => exit());
172 173 174
		}, 1000);
	}

A
Alex Dima 已提交
175
	// Handle "eager" activation extensions
A
Alex Dima 已提交
176
	private handleEagerExtensions(): Promise<void> {
177
		this._extensionService.activateByEvent('*', true).then(null, (err) => {
E
Erich Gamma 已提交
178 179
			console.error(err);
		});
B
Benjamin Pasero 已提交
180

A
Alex Dima 已提交
181
		return this.handleWorkspaceContainsEagerExtensions();
E
Erich Gamma 已提交
182 183
	}

A
Alex Dima 已提交
184
	private handleWorkspaceContainsEagerExtensions(): Promise<void> {
S
Sandeep Somavarapu 已提交
185
		if (!this._workspace || this._workspace.folders.length === 0) {
A
Alex Dima 已提交
186
			return Promise.resolve(null);
E
Erich Gamma 已提交
187 188
		}

A
Alex Dima 已提交
189
		return Promise.all(
A
Alex Dima 已提交
190 191 192 193 194
			this._extensionService.getAllExtensionDescriptions().map((desc) => {
				return this.handleWorkspaceContainsEagerExtension(desc);
			})
		).then(() => { });
	}
E
Erich Gamma 已提交
195

A
Alex Dima 已提交
196
	private handleWorkspaceContainsEagerExtension(desc: IExtensionDescription): Promise<void> {
B
Benjamin Pasero 已提交
197
		const activationEvents = desc.activationEvents;
A
Alex Dima 已提交
198
		if (!activationEvents) {
A
Alex Dima 已提交
199
			return Promise.resolve(void 0);
A
Alex Dima 已提交
200
		}
E
Erich Gamma 已提交
201

A
Alex Dima 已提交
202 203 204 205 206
		const fileNames: string[] = [];
		const globPatterns: string[] = [];

		for (let i = 0; i < activationEvents.length; i++) {
			if (/^workspaceContains:/.test(activationEvents[i])) {
B
Benjamin Pasero 已提交
207
				const fileNameOrGlob = activationEvents[i].substr('workspaceContains:'.length);
A
Alex Dima 已提交
208 209 210 211
				if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) {
					globPatterns.push(fileNameOrGlob);
				} else {
					fileNames.push(fileNameOrGlob);
E
Erich Gamma 已提交
212 213
				}
			}
A
Alex Dima 已提交
214
		}
E
Erich Gamma 已提交
215

A
Alex Dima 已提交
216
		if (fileNames.length === 0 && globPatterns.length === 0) {
A
Alex Dima 已提交
217
			return Promise.resolve(void 0);
A
Alex Dima 已提交
218
		}
219

A
Alex Dima 已提交
220
		const fileNamePromise = Promise.all(fileNames.map((fileName) => this.activateIfFileName(desc.id, fileName))).then(() => { });
B
Benjamin Pasero 已提交
221
		const globPatternPromise = this.activateIfGlobPatterns(desc.id, globPatterns);
A
Alex Dima 已提交
222

A
Alex Dima 已提交
223
		return Promise.all([fileNamePromise, globPatternPromise]).then(() => { });
A
Alex Dima 已提交
224 225
	}

226
	private async activateIfFileName(extensionId: string, fileName: string): Promise<void> {
A
Alex Dima 已提交
227

B
Benjamin Pasero 已提交
228
		// find exact path
A
Alex Dima 已提交
229
		for (const { uri } of this._workspace.folders) {
230
			if (await pfs.exists(join(URI.revive(uri).fsPath, fileName))) {
A
Alex Dima 已提交
231 232
				// the file was found
				return (
A
Alex Dima 已提交
233
					this._extensionService.activateById(extensionId, new ExtensionActivatedByEvent(true, `workspaceContains:${fileName}`))
234
						.then(null, err => console.error(err))
A
Alex Dima 已提交
235
				);
236
			}
A
Alex Dima 已提交
237 238 239 240
		}

		return undefined;
	}
J
Joao Moreno 已提交
241

242
	private async activateIfGlobPatterns(extensionId: string, globPatterns: string[]): Promise<void> {
243 244
		this._extHostLogService.trace(`extensionHostMain#activateIfGlobPatterns: fileSearch, extension: ${extensionId}, entryPoint: workspaceContains`);

245
		if (globPatterns.length === 0) {
A
Alex Dima 已提交
246
			return Promise.resolve(void 0);
247 248
		}

R
Rob Lourens 已提交
249 250
		const tokenSource = new CancellationTokenSource();
		const searchP = this._mainThreadWorkspace.$checkExists(globPatterns, tokenSource.token);
251 252

		const timer = setTimeout(async () => {
R
Rob Lourens 已提交
253
			tokenSource.cancel();
254
			this._extensionService.activateById(extensionId, new ExtensionActivatedByEvent(true, `workspaceContainsTimeout:${globPatterns.join(',')}`))
255 256 257
				.then(null, err => console.error(err));
		}, ExtensionHostMain.WORKSPACE_CONTAINS_TIMEOUT);

R
Rob Lourens 已提交
258 259 260 261 262 263 264 265 266 267
		let exists: boolean;
		try {
			exists = await searchP;
		} catch (err) {
			if (!errors.isPromiseCanceledError(err)) {
				console.error(err);
			}
		}

		tokenSource.dispose();
268
		clearTimeout(timer);
R
Rob Lourens 已提交
269

270
		if (exists) {
A
Alex Dima 已提交
271 272
			// a file was found matching one of the glob patterns
			return (
A
Alex Dima 已提交
273
				this._extensionService.activateById(extensionId, new ExtensionActivatedByEvent(true, `workspaceContains:${globPatterns.join(',')}`))
274
					.then(null, err => console.error(err))
A
Alex Dima 已提交
275 276 277
			);
		}

A
Alex Dima 已提交
278
		return Promise.resolve(void 0);
E
Erich Gamma 已提交
279 280
	}

A
Alex Dima 已提交
281
	private handleExtensionTests(): Promise<void> {
282
		if (!this._environment.extensionTestsPath || !this._environment.extensionDevelopmentLocationURI) {
A
Alex Dima 已提交
283
			return Promise.resolve(null);
E
Erich Gamma 已提交
284 285 286
		}

		// Require the test runner via node require from the provided path
B
Benjamin Pasero 已提交
287 288
		let testRunner: ITestRunner;
		let requireError: Error;
E
Erich Gamma 已提交
289
		try {
290
			testRunner = <any>require.__$__nodeRequire(this._environment.extensionTestsPath);
E
Erich Gamma 已提交
291 292 293 294 295 296
		} catch (error) {
			requireError = error;
		}

		// Execute the runner if it follows our spec
		if (testRunner && typeof testRunner.run === 'function') {
297
			return new Promise<void>((c, e) => {
298
				testRunner.run(this._environment.extensionTestsPath, (error, failures) => {
E
Erich Gamma 已提交
299 300 301 302 303 304
					if (error) {
						e(error.toString());
					} else {
						c(null);
					}

305
					// after tests have run, we shutdown the host
306
					this.gracefulExit(failures && failures > 0 ? 1 /* ERROR */ : 0 /* OK */);
E
Erich Gamma 已提交
307 308 309 310
				});
			});
		}

311 312
		// Otherwise make sure to shutdown anyway even in case of an error
		else {
313
			this.gracefulExit(1 /* ERROR */);
314
		}
E
Erich Gamma 已提交
315

A
Alex Dima 已提交
316
		return Promise.reject(new Error(requireError ? requireError.toString() : nls.localize('extensionTestError', "Path {0} does not point to a valid extension test runner.", this._environment.extensionTestsPath)));
317 318
	}

319
	private transform(initData: IInitData, rpcProtocol: RPCProtocol): IInitData {
A
Alex Dima 已提交
320 321 322 323 324
		initData.extensions.forEach((ext) => (<any>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation)));
		initData.environment.appRoot = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appRoot));
		initData.environment.appSettingsHome = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appSettingsHome));
		initData.environment.extensionDevelopmentLocationURI = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.extensionDevelopmentLocationURI));
		initData.logsLocation = URI.revive(rpcProtocol.transformIncomingURIs(initData.logsLocation));
325
		initData.workspace = rpcProtocol.transformIncomingURIs(initData.workspace);
326
		return initData;
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
}