terminalTaskSystem.ts 55.0 KB
Newer Older
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 path from 'vs/base/common/path';
7 8
import * as nls from 'vs/nls';
import * as Objects from 'vs/base/common/objects';
9
import * as Types from 'vs/base/common/types';
10 11
import * as Platform from 'vs/base/common/platform';
import * as Async from 'vs/base/common/async';
12
import * as resources from 'vs/base/common/resources';
13
import { IStringDictionary, values } from 'vs/base/common/collections';
14
import { LinkedMap, Touch } from 'vs/base/common/map';
15
import Severity from 'vs/base/common/severity';
M
Matt Bierner 已提交
16
import { Event, Emitter } from 'vs/base/common/event';
M
Matt Bierner 已提交
17
import { DisposableStore } from 'vs/base/common/lifecycle';
B
Benjamin Pasero 已提交
18
import { isUNC } from 'vs/base/common/extpath';
19

20
import { IFileService } from 'vs/platform/files/common/files';
21
import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers';
22
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
23
import { IModelService } from 'vs/editor/common/services/modelService';
24
import { ProblemMatcher, ProblemMatcherRegistry /*, ProblemPattern, getResource */ } from 'vs/workbench/contrib/tasks/common/problemMatcher';
25
import Constants from 'vs/workbench/contrib/markers/browser/constants';
26

27 28 29
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';

import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
30
import { ITerminalService, ITerminalInstance, IShellLaunchConfig } from 'vs/workbench/contrib/terminal/common/terminal';
31
import { IOutputService } from 'vs/workbench/contrib/output/common/output';
32
import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEventKind, ProblemHandlingStrategy } from 'vs/workbench/contrib/tasks/common/problemCollectors';
33 34
import {
	Task, CustomTask, ContributedTask, RevealKind, CommandOptions, ShellConfiguration, RuntimeType, PanelKind,
35
	TaskEvent, TaskEventKind, ShellQuotingOptions, ShellQuoting, CommandString, CommandConfiguration, ExtensionTaskSource, TaskScope, RevealProblemKind, DependsOrder
36
} from 'vs/workbench/contrib/tasks/common/tasks';
37
import {
38
	ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, ITaskResolver,
A
Alex Ross 已提交
39
	TelemetryEvent, Triggers, TaskTerminateResponse, TaskSystemInfoResolver, TaskSystemInfo, ResolveSet, ResolvedVariables
40
} from 'vs/workbench/contrib/tasks/common/taskSystem';
A
Alex Ross 已提交
41
import { URI } from 'vs/base/common/uri';
42
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
A
Alex Ross 已提交
43
import { Schemas } from 'vs/base/common/network';
44
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
45
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
A
Alex Ross 已提交
46
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
47
import { env as processEnv, cwd as processCwd } from 'vs/base/common/process';
48

D
Dirk Baeumer 已提交
49 50 51
interface TerminalData {
	terminal: ITerminalInstance;
	lastTask: string;
52
	group?: string;
D
Dirk Baeumer 已提交
53 54 55 56
}

interface ActiveTerminalData {
	terminal: ITerminalInstance;
57
	task: Task;
58
	promise: Promise<ITaskSummary>;
D
Dirk Baeumer 已提交
59 60
}

61 62
class VariableResolver {

63
	constructor(public workspaceFolder: IWorkspaceFolder | undefined, public taskSystemInfo: TaskSystemInfo | undefined, private _values: Map<string, string>, private _service: IConfigurationResolverService | undefined) {
64 65 66
	}
	resolve(value: string): string {
		return value.replace(/\$\{(.*?)\}/g, (match: string, variable: string) => {
A
Alex Ross 已提交
67 68
			// Strip out the ${} because the map contains them variables without those characters.
			let result = this._values.get(match.substring(2, match.length - 1));
69
			if ((result !== undefined) && (result !== null)) {
70 71 72 73 74 75 76 77 78 79
				return result;
			}
			if (this._service) {
				return this._service.resolve(this.workspaceFolder, match);
			}
			return match;
		});
	}
}

A
Alex Ross 已提交
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
export class VerifiedTask {
	readonly task: Task;
	readonly resolver: ITaskResolver;
	readonly trigger: string;
	resolvedVariables?: ResolvedVariables;
	systemInfo?: TaskSystemInfo;
	workspaceFolder?: IWorkspaceFolder;
	shellLaunchConfig?: IShellLaunchConfig;

	constructor(task: Task, resolver: ITaskResolver, trigger: string) {
		this.task = task;
		this.resolver = resolver;
		this.trigger = trigger;
	}

	public verify(): boolean {
96 97 98 99 100
		let verified = false;
		if (this.trigger && this.resolvedVariables && this.workspaceFolder && (this.shellLaunchConfig !== undefined)) {
			verified = true;
		}
		return verified;
A
Alex Ross 已提交
101 102 103 104
	}

	public getVerifiedTask(): { task: Task, resolver: ITaskResolver, trigger: string, resolvedVariables: ResolvedVariables, systemInfo: TaskSystemInfo, workspaceFolder: IWorkspaceFolder, shellLaunchConfig: IShellLaunchConfig } {
		if (this.verify()) {
105
			return { task: this.task, resolver: this.resolver, trigger: this.trigger, resolvedVariables: this.resolvedVariables!, systemInfo: this.systemInfo!, workspaceFolder: this.workspaceFolder!, shellLaunchConfig: this.shellLaunchConfig! };
A
Alex Ross 已提交
106 107 108 109 110 111
		} else {
			throw new Error('VerifiedTask was not checked. verify must be checked before getVerifiedTask.');
		}
	}
}

112
export class TerminalTaskSystem implements ITaskSystem {
113 114 115

	public static TelemetryEventName: string = 'taskService';

A
Alex Ross 已提交
116
	private static ProcessVarName = '__process__';
117

118 119 120 121 122 123 124
	private static shellQuotes: IStringDictionary<ShellQuotingOptions> = {
		'cmd': {
			strong: '"'
		},
		'powershell': {
			escape: {
				escapeChar: '`',
125
				charsToEscape: ' "\'()'
126 127 128 129 130
			},
			strong: '\'',
			weak: '"'
		},
		'bash': {
131 132 133 134
			escape: {
				escapeChar: '\\',
				charsToEscape: ' "\''
			},
135 136 137 138
			strong: '\'',
			weak: '"'
		},
		'zsh': {
139 140 141 142
			escape: {
				escapeChar: '\\',
				charsToEscape: ' "\''
			},
143 144 145 146 147 148
			strong: '\'',
			weak: '"'
		}
	};

	private static osShellQuotes: IStringDictionary<ShellQuotingOptions> = {
A
Alex Ross 已提交
149 150 151
		'Linux': TerminalTaskSystem.shellQuotes['bash'],
		'Mac': TerminalTaskSystem.shellQuotes['bash'],
		'Windows': TerminalTaskSystem.shellQuotes['powershell']
152 153
	};

D
Dirk Baeumer 已提交
154 155
	private activeTasks: IStringDictionary<ActiveTerminalData>;
	private terminals: IStringDictionary<TerminalData>;
156 157
	private idleTaskTerminals: LinkedMap<string, string>;
	private sameTaskTerminals: IStringDictionary<string>;
A
Alex Ross 已提交
158
	private taskSystemInfoResolver: TaskSystemInfoResolver;
159 160 161 162
	private lastTask: VerifiedTask | undefined;
	// Should always be set in run
	private currentTask!: VerifiedTask;
	private isRerun: boolean = false;
163

M
Matt Bierner 已提交
164
	private readonly _onDidStateChange: Emitter<TaskEvent>;
165

166 167 168
	constructor(
		private terminalService: ITerminalService,
		private outputService: IOutputService,
169
		private panelService: IPanelService,
170 171 172
		private markerService: IMarkerService, private modelService: IModelService,
		private configurationResolverService: IConfigurationResolverService,
		private telemetryService: ITelemetryService,
173
		private contextService: IWorkspaceContextService,
174
		private environmentService: IWorkbenchEnvironmentService,
175
		private outputChannelId: string,
176
		private fileService: IFileService,
177
		private terminalInstanceService: ITerminalInstanceService,
A
Alex Ross 已提交
178
		private remoteAgentService: IRemoteAgentService,
A
Alex Ross 已提交
179
		taskSystemInfoResolver: TaskSystemInfoResolver,
180
	) {
181 182

		this.activeTasks = Object.create(null);
D
Dirk Baeumer 已提交
183
		this.terminals = Object.create(null);
184 185
		this.idleTaskTerminals = new LinkedMap<string, string>();
		this.sameTaskTerminals = Object.create(null);
186 187

		this._onDidStateChange = new Emitter();
188
		this.taskSystemInfoResolver = taskSystemInfoResolver;
189 190 191 192
	}

	public get onDidStateChange(): Event<TaskEvent> {
		return this._onDidStateChange.event;
193 194 195
	}

	public log(value: string): void {
196
		this.appendOutput(value + '\n');
197 198
	}

199
	protected showOutput(): void {
200
		this.outputService.showChannel(this.outputChannelId, true);
201 202
	}

203
	public run(task: Task, resolver: ITaskResolver, trigger: string = Triggers.command): ITaskExecuteResult {
A
Alex Ross 已提交
204
		this.currentTask = new VerifiedTask(task, resolver, trigger);
A
Alex Ross 已提交
205
		let terminalData = this.activeTasks[task.getMapKey()];
D
Dirk Baeumer 已提交
206
		if (terminalData && terminalData.promise) {
207 208 209
			let reveal = RevealKind.Always;
			let focus = false;
			if (CustomTask.is(task) || ContributedTask.is(task)) {
210 211
				reveal = task.command.presentation!.reveal;
				focus = task.command.presentation!.focus;
212
			}
213 214 215
			if (reveal === RevealKind.Always || focus) {
				this.terminalService.setActiveInstance(terminalData.terminal);
				this.terminalService.showPanel(focus);
D
Dirk Baeumer 已提交
216
			}
A
Alex Ross 已提交
217
			this.lastTask = this.currentTask;
218
			return { kind: TaskExecuteKind.Active, task, active: { same: true, background: task.configurationProperties.isBackground! }, promise: terminalData.promise };
D
Dirk Baeumer 已提交
219 220
		}

221
		try {
A
Alex Ross 已提交
222
			const executeResult = { kind: TaskExecuteKind.Started, task, started: {}, promise: this.executeTask(task, resolver, trigger) };
223 224 225
			executeResult.promise.then(summary => {
				this.lastTask = this.currentTask;
			});
A
Alex Ross 已提交
226
			return executeResult;
227 228 229 230 231 232 233 234 235 236 237 238 239
		} catch (error) {
			if (error instanceof TaskError) {
				throw error;
			} else if (error instanceof Error) {
				this.log(error.message);
				throw new TaskError(Severity.Error, error.message, TaskErrors.UnknownError);
			} else {
				this.log(error.toString());
				throw new TaskError(Severity.Error, nls.localize('TerminalTaskSystem.unknownError', 'A unknown error has occurred while executing a task. See task output log for details.'), TaskErrors.UnknownError);
			}
		}
	}

A
Alex Ross 已提交
240 241
	public rerun(): ITaskExecuteResult | undefined {
		if (this.lastTask && this.lastTask.verify()) {
R
Rob Lourens 已提交
242
			if ((this.lastTask.task.runOptions.reevaluateOnRerun !== undefined) && !this.lastTask.task.runOptions.reevaluateOnRerun) {
A
Alex Ross 已提交
243 244 245
				this.isRerun = true;
			}
			const result = this.run(this.lastTask.task, this.lastTask.resolver);
246 247 248
			result.promise.then(summary => {
				this.isRerun = false;
			});
A
Alex Ross 已提交
249 250 251 252 253
			return result;
		} else {
			return undefined;
		}
	}
254 255

	public revealTask(task: Task): boolean {
A
Alex Ross 已提交
256
		let terminalData = this.activeTasks[task.getMapKey()];
257 258 259 260
		if (!terminalData) {
			return false;
		}
		this.terminalService.setActiveInstance(terminalData.terminal);
261
		if (CustomTask.is(task) || ContributedTask.is(task)) {
262
			this.terminalService.showPanel(task.command.presentation!.focus);
263
		}
264 265 266
		return true;
	}

267 268
	public isActive(): Promise<boolean> {
		return Promise.resolve(this.isActiveSync());
269 270 271
	}

	public isActiveSync(): boolean {
D
Dirk Baeumer 已提交
272
		return Object.keys(this.activeTasks).length > 0;
273 274 275
	}

	public canAutoTerminate(): boolean {
A
Alex Ross 已提交
276
		return Object.keys(this.activeTasks).every(key => !this.activeTasks[key].task.configurationProperties.promptOnClose);
277 278
	}

279 280 281 282
	public getActiveTasks(): Task[] {
		return Object.keys(this.activeTasks).map(key => this.activeTasks[key].task);
	}

G
Gabriel DeBacker 已提交
283
	public customExecutionComplete(task: Task, result: number): Promise<void> {
284 285
		let activeTerminal = this.activeTasks[task.getMapKey()];
		if (!activeTerminal) {
286
			return Promise.reject(new Error('Expected to have a terminal for an custom execution task'));
287 288 289
		}

		return new Promise<void>((resolve) => {
D
Daniel Imms 已提交
290
			// activeTerminal.terminal.rendererExit(result);
291 292 293 294
			resolve();
		});
	}

295
	public terminate(task: Task): Promise<TaskTerminateResponse> {
A
Alex Ross 已提交
296
		let activeTerminal = this.activeTasks[task.getMapKey()];
297
		if (!activeTerminal) {
298
			return Promise.resolve<TaskTerminateResponse>({ success: false, task: undefined });
299
		}
300
		return new Promise<TaskTerminateResponse>((resolve, reject) => {
301
			let terminal = activeTerminal.terminal;
302

303
			const onExit = terminal.onExit(() => {
304 305 306
				let task = activeTerminal.task;
				try {
					onExit.dispose();
307
					this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Terminated, task));
308 309 310 311
				} catch (error) {
					// Do nothing.
				}
				resolve({ success: true, task: task });
312 313 314
			});
			terminal.dispose();
		});
315 316
	}

317 318
	public terminateAll(): Promise<TaskTerminateResponse[]> {
		let promises: Promise<TaskTerminateResponse>[] = [];
D
Dirk Baeumer 已提交
319
		Object.keys(this.activeTasks).forEach((key) => {
320 321
			let terminalData = this.activeTasks[key];
			let terminal = terminalData.terminal;
322
			promises.push(new Promise<TaskTerminateResponse>((resolve, reject) => {
323 324 325 326
				const onExit = terminal.onExit(() => {
					let task = terminalData.task;
					try {
						onExit.dispose();
327
						this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Terminated, task));
328 329 330 331 332 333 334
					} catch (error) {
						// Do nothing.
					}
					resolve({ success: true, task: terminalData.task });
				});
			}));
			terminal.dispose();
D
Dirk Baeumer 已提交
335 336
		});
		this.activeTasks = Object.create(null);
337
		return Promise.all<TaskTerminateResponse>(promises);
338 339
	}

340
	private async executeTask(task: Task, resolver: ITaskResolver, trigger: string): Promise<ITaskSummary> {
341
		let promises: Promise<ITaskSummary>[] = [];
A
Alex Ross 已提交
342
		if (task.configurationProperties.dependsOn) {
343
			for (const dependency of task.configurationProperties.dependsOn) {
344 345 346
				let dependencyTask = resolver.resolve(dependency.workspaceFolder, dependency.task!);
				if (dependencyTask) {
					let key = dependencyTask.getMapKey();
347
					let promise = this.activeTasks[key] ? this.activeTasks[key].promise : undefined;
D
Dirk Baeumer 已提交
348
					if (!promise) {
349 350
						this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.DependsOnStarted, task));
						promise = this.executeTask(dependencyTask, resolver, trigger);
D
Dirk Baeumer 已提交
351
					}
352 353 354
					if (task.configurationProperties.dependsOrder === DependsOrder.sequence) {
						promise = Promise.resolve(await promise);
					}
D
Dirk Baeumer 已提交
355
					promises.push(promise);
356
				} else {
357 358 359 360 361
					this.log(nls.localize('dependencyFailed',
						'Couldn\'t resolve dependent task \'{0}\' in workspace folder \'{1}\'',
						Types.isString(dependency.task) ? dependency.task : JSON.stringify(dependency.task, undefined, 0),
						dependency.workspaceFolder.name
					));
362
					this.showOutput();
D
Dirk Baeumer 已提交
363
				}
364
			}
D
Dirk Baeumer 已提交
365 366
		}

367
		if ((ContributedTask.is(task) || CustomTask.is(task)) && (task.command)) {
368
			return Promise.all(promises).then((summaries): Promise<ITaskSummary> | ITaskSummary => {
D
Dirk Baeumer 已提交
369 370 371 372 373
				for (let summary of summaries) {
					if (summary.exitCode !== 0) {
						return { exitCode: summary.exitCode };
					}
				}
A
Alex Ross 已提交
374 375 376 377 378
				if (this.isRerun) {
					return this.reexecuteCommand(task, trigger);
				} else {
					return this.executeCommand(task, trigger);
				}
D
Dirk Baeumer 已提交
379
			});
380
		} else {
381
			return Promise.all(promises).then((summaries): ITaskSummary => {
D
Dirk Baeumer 已提交
382 383 384 385 386 387 388 389 390 391
				for (let summary of summaries) {
					if (summary.exitCode !== 0) {
						return { exitCode: summary.exitCode };
					}
				}
				return { exitCode: 0 };
			});
		}
	}

392
	private resolveVariablesFromSet(taskSystemInfo: TaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, variables: Set<string>): Promise<ResolvedVariables> {
393 394 395 396 397 398 399 400 401 402 403 404 405 406
		let isProcess = task.command && task.command.runtime === RuntimeType.Process;
		let options = task.command && task.command.options ? task.command.options : undefined;
		let cwd = options ? options.cwd : undefined;
		let envPath: string | undefined = undefined;
		if (options && options.env) {
			for (let key of Object.keys(options.env)) {
				if (key.toLowerCase() === 'path') {
					if (Types.isString(options.env[key])) {
						envPath = options.env[key];
					}
					break;
				}
			}
		}
A
Alex Ross 已提交
407

408
		let resolvedVariables: Promise<ResolvedVariables>;
409
		if (taskSystemInfo && workspaceFolder) {
410 411 412
			let resolveSet: ResolveSet = {
				variables
			};
A
Alex Ross 已提交
413

414
			if (taskSystemInfo.platform === Platform.Platform.Windows && isProcess) {
415
				resolveSet.process = { name: CommandString.value(task.command.name!) };
416 417 418 419 420 421 422
				if (cwd) {
					resolveSet.process.cwd = cwd;
				}
				if (envPath) {
					resolveSet.process.path = envPath;
				}
			}
A
Alex Ross 已提交
423 424
			resolvedVariables = taskSystemInfo.resolveVariables(workspaceFolder, resolveSet).then(resolved => {
				if ((taskSystemInfo.platform !== Platform.Platform.Windows) && isProcess) {
425
					resolved.variables.set(TerminalTaskSystem.ProcessVarName, CommandString.value(task.command.name!));
A
Alex Ross 已提交
426 427 428
				}
				return Promise.resolve(resolved);
			});
A
Alex Ross 已提交
429
			return resolvedVariables;
430
		} else {
A
Alex Ross 已提交
431 432 433 434
			let variablesArray = new Array<string>();
			variables.forEach(variable => variablesArray.push(variable));

			return new Promise((resolve, reject) => {
A
Alex Ross 已提交
435
				this.configurationResolverService.resolveWithInteraction(workspaceFolder, variablesArray, 'tasks').then(async resolvedVariablesMap => {
436 437 438 439
					if (resolvedVariablesMap) {
						if (isProcess) {
							let processVarValue: string;
							if (Platform.isWindows) {
440
								processVarValue = await this.findExecutable(
441
									this.configurationResolverService.resolve(workspaceFolder, CommandString.value(task.command.name!)),
442 443 444 445
									cwd ? this.configurationResolverService.resolve(workspaceFolder, cwd) : undefined,
									envPath ? envPath.split(path.delimiter).map(p => this.configurationResolverService.resolve(workspaceFolder, p)) : undefined
								);
							} else {
446
								processVarValue = this.configurationResolverService.resolve(workspaceFolder, CommandString.value(task.command.name!));
447 448
							}
							resolvedVariablesMap.set(TerminalTaskSystem.ProcessVarName, processVarValue);
A
Alex Ross 已提交
449
						}
450 451 452 453 454 455
						let resolvedVariablesResult: ResolvedVariables = {
							variables: resolvedVariablesMap,
						};
						resolve(resolvedVariablesResult);
					} else {
						resolve(undefined);
A
Alex Ross 已提交
456 457 458 459
					}
				}, reason => {
					reject(reason);
				});
460 461
			});
		}
A
Alex Ross 已提交
462 463
	}

464
	private executeCommand(task: CustomTask | ContributedTask, trigger: string): Promise<ITaskSummary> {
465
		const workspaceFolder = this.currentTask.workspaceFolder = task.getWorkspaceFolder();
466
		const systemInfo: TaskSystemInfo | undefined = this.currentTask.systemInfo = workspaceFolder ? this.taskSystemInfoResolver(workspaceFolder) : undefined;
A
Alex Ross 已提交
467 468 469

		let variables = new Set<string>();
		this.collectTaskVariables(variables, task);
470
		const resolvedVariables = this.resolveVariablesFromSet(systemInfo, workspaceFolder, task, variables);
A
Alex Ross 已提交
471 472

		return resolvedVariables.then((resolvedVariables) => {
A
Alex Ross 已提交
473
			const isCustomExecution = (task.command.runtime === RuntimeType.CustomExecution2);
474
			if (resolvedVariables && (task.command !== undefined) && task.command.runtime && (isCustomExecution || (task.command.name !== undefined))) {
475
				this.currentTask.resolvedVariables = resolvedVariables;
476
				return this.executeInTerminal(task, trigger, new VariableResolver(workspaceFolder, systemInfo, resolvedVariables.variables, this.configurationResolverService), workspaceFolder);
477 478 479
			} else {
				return Promise.resolve({ exitCode: 0 });
			}
A
Alex Ross 已提交
480 481
		}, reason => {
			return Promise.reject(reason);
482 483 484
		});
	}

485
	private reexecuteCommand(task: CustomTask | ContributedTask, trigger: string): Promise<ITaskSummary> {
486 487 488 489 490
		const lastTask = this.lastTask;
		if (!lastTask) {
			return Promise.reject(new Error('No task previously run'));
		}
		const workspaceFolder = this.currentTask.workspaceFolder = lastTask.workspaceFolder;
A
Alex Ross 已提交
491 492 493 494 495 496
		let variables = new Set<string>();
		this.collectTaskVariables(variables, task);

		// Check that the task hasn't changed to include new variables
		let hasAllVariables = true;
		variables.forEach(value => {
497
			if (value.substring(2, value.length - 1) in lastTask.getVerifiedTask().resolvedVariables) {
A
Alex Ross 已提交
498 499 500 501 502
				hasAllVariables = false;
			}
		});

		if (!hasAllVariables) {
503
			return this.resolveVariablesFromSet(lastTask.getVerifiedTask().systemInfo, lastTask.getVerifiedTask().workspaceFolder, task, variables).then((resolvedVariables) => {
A
Alex Ross 已提交
504
				this.currentTask.resolvedVariables = resolvedVariables;
505
				return this.executeInTerminal(task, trigger, new VariableResolver(lastTask.getVerifiedTask().workspaceFolder, lastTask.getVerifiedTask().systemInfo, resolvedVariables.variables, this.configurationResolverService), workspaceFolder!);
A
Alex Ross 已提交
506 507
			}, reason => {
				return Promise.reject(reason);
A
Alex Ross 已提交
508 509
			});
		} else {
510 511
			this.currentTask.resolvedVariables = lastTask.getVerifiedTask().resolvedVariables;
			return this.executeInTerminal(task, trigger, new VariableResolver(lastTask.getVerifiedTask().workspaceFolder, lastTask.getVerifiedTask().systemInfo, lastTask.getVerifiedTask().resolvedVariables.variables, this.configurationResolverService), workspaceFolder!);
A
Alex Ross 已提交
512 513 514
		}
	}

515
	private async executeInTerminal(task: CustomTask | ContributedTask, trigger: string, resolver: VariableResolver, workspaceFolder: IWorkspaceFolder | undefined): Promise<ITaskSummary> {
516 517 518
		let terminal: ITerminalInstance | undefined = undefined;
		let executedCommand: string | undefined = undefined;
		let error: TaskError | undefined = undefined;
519
		let promise: Promise<ITaskSummary> | undefined = undefined;
A
Alex Ross 已提交
520
		if (task.configurationProperties.isBackground) {
521 522
			const problemMatchers = this.resolveMatchers(resolver, task.configurationProperties.problemMatchers);
			let watchingProblemMatcher = new WatchingProblemCollector(problemMatchers, this.markerService, this.modelService, this.fileService);
M
Matt Bierner 已提交
523
			const toDispose = new DisposableStore();
524
			let eventCounter: number = 0;
M
Matt Bierner 已提交
525
			toDispose.add(watchingProblemMatcher.onDidStateChange((event) => {
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541
				if (event.kind === ProblemCollectorEventKind.BackgroundProcessingBegins) {
					eventCounter++;
					this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Active, task));
				} else if (event.kind === ProblemCollectorEventKind.BackgroundProcessingEnds) {
					eventCounter--;
					this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Inactive, task));
					if (eventCounter === 0) {
						if ((watchingProblemMatcher.numberOfMatches > 0) && watchingProblemMatcher.maxMarkerSeverity &&
							(watchingProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error)) {
							let reveal = task.command.presentation!.reveal;
							let revealProblems = task.command.presentation!.revealProblems;
							if (revealProblems === RevealProblemKind.OnProblem) {
								this.panelService.openPanel(Constants.MARKERS_PANEL_ID, true);
							} else if (reveal === RevealKind.Silent) {
								this.terminalService.setActiveInstance(terminal!);
								this.terminalService.showPanel(false);
542 543
							}
						}
544
					}
545
				}
546 547 548
			}));
			watchingProblemMatcher.aboutToStart();
			let delayer: Async.Delayer<any> | undefined = undefined;
549
			[terminal, executedCommand, error] = await this.createTerminal(task, resolver, workspaceFolder);
550 551 552 553 554 555 556 557 558 559 560

			if (error) {
				return Promise.reject(new Error((<TaskError>error).message));
			}
			if (!terminal) {
				return Promise.reject(new Error(`Failed to create terminal for task ${task._label}`));
			}

			let processStartedSignaled = false;
			terminal.processReady.then(() => {
				if (!processStartedSignaled) {
A
Alex Ross 已提交
561
					this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!));
562 563 564 565 566 567 568
					processStartedSignaled = true;
				}
			}, (_error) => {
				// The process never got ready. Need to think how to handle this.
			});
			this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Start, task, terminal.id));
			const registeredLinkMatchers = this.registerLinkMatchers(terminal, problemMatchers);
569
			let skipLine: boolean = (!!task.command.presentation && task.command.presentation.echo);
570
			const onData = terminal.onLineData((line) => {
571 572 573 574
				if (skipLine) {
					skipLine = false;
					return;
				}
575 576 577 578 579 580 581
				watchingProblemMatcher.processLine(line);
				if (!delayer) {
					delayer = new Async.Delayer(3000);
				}
				delayer.trigger(() => {
					watchingProblemMatcher.forceDelivery();
					delayer = undefined;
D
Dirk Baeumer 已提交
582
				});
583 584 585
			});
			promise = new Promise<ITaskSummary>((resolve, reject) => {
				const onExit = terminal!.onExit((exitCode) => {
D
Dirk Baeumer 已提交
586 587
					onData.dispose();
					onExit.dispose();
A
Alex Ross 已提交
588
					let key = task.getMapKey();
589
					delete this.activeTasks[key];
590
					this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Changed));
591 592 593 594 595 596 597 598 599 600
					if (exitCode !== undefined) {
						// Only keep a reference to the terminal if it is not being disposed.
						switch (task.command.presentation!.panel) {
							case PanelKind.Dedicated:
								this.sameTaskTerminals[key] = terminal!.id.toString();
								break;
							case PanelKind.Shared:
								this.idleTaskTerminals.set(key, terminal!.id.toString(), Touch.AsOld);
								break;
						}
D
Dirk Baeumer 已提交
601
					}
602 603 604 605
					let reveal = task.command.presentation!.reveal;
					if ((reveal === RevealKind.Silent) && ((exitCode !== 0) || (watchingProblemMatcher.numberOfMatches > 0) && watchingProblemMatcher.maxMarkerSeverity &&
						(watchingProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error))) {
						this.terminalService.setActiveInstance(terminal!);
606 607
						this.terminalService.showPanel(false);
					}
D
Dirk Baeumer 已提交
608
					watchingProblemMatcher.done();
D
Dirk Baeumer 已提交
609
					watchingProblemMatcher.dispose();
610
					registeredLinkMatchers.forEach(handle => terminal!.deregisterLinkMatcher(handle));
611
					if (!processStartedSignaled) {
612
						this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!));
613
						processStartedSignaled = true;
614
					}
G
Gabriel DeBacker 已提交
615

A
Alex Ross 已提交
616
					this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode));
G
Gabriel DeBacker 已提交
617

D
Dirk Baeumer 已提交
618
					for (let i = 0; i < eventCounter; i++) {
619 620
						let event = TaskEvent.create(TaskEventKind.Inactive, task);
						this._onDidStateChange.fire(event);
D
Dirk Baeumer 已提交
621 622
					}
					eventCounter = 0;
623
					this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task));
M
Matt Bierner 已提交
624
					toDispose.dispose();
D
Dirk Baeumer 已提交
625 626 627 628
					resolve({ exitCode });
				});
			});
		} else {
629
			[terminal, executedCommand, error] = await this.createTerminal(task, resolver, workspaceFolder);
630

631 632 633 634 635 636 637 638 639 640
			if (error) {
				return Promise.reject(new Error((<TaskError>error).message));
			}
			if (!terminal) {
				return Promise.reject(new Error(`Failed to create terminal for task ${task._label}`));
			}

			let processStartedSignaled = false;
			terminal.processReady.then(() => {
				if (!processStartedSignaled) {
A
Alex Ross 已提交
641
					this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!));
642 643 644 645 646 647 648 649 650 651
					processStartedSignaled = true;
				}
			}, (_error) => {
				// The process never got ready. Need to think how to handle this.
			});
			this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Start, task, terminal.id));
			this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Active, task));
			let problemMatchers = this.resolveMatchers(resolver, task.configurationProperties.problemMatchers);
			let startStopProblemMatcher = new StartStopProblemCollector(problemMatchers, this.markerService, this.modelService, ProblemHandlingStrategy.Clean, this.fileService);
			const registeredLinkMatchers = this.registerLinkMatchers(terminal, problemMatchers);
652
			let skipLine: boolean = (!!task.command.presentation && task.command.presentation.echo);
653
			const onData = terminal.onLineData((line) => {
654 655 656 657
				if (skipLine) {
					skipLine = false;
					return;
				}
658 659 660 661
				startStopProblemMatcher.processLine(line);
			});
			promise = new Promise<ITaskSummary>((resolve, reject) => {
				const onExit = terminal!.onExit((exitCode) => {
D
Dirk Baeumer 已提交
662 663
					onData.dispose();
					onExit.dispose();
A
Alex Ross 已提交
664
					let key = task.getMapKey();
665
					delete this.activeTasks[key];
666
					this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Changed));
667 668 669 670 671 672 673 674 675 676
					if (exitCode !== undefined) {
						// Only keep a reference to the terminal if it is not being disposed.
						switch (task.command.presentation!.panel) {
							case PanelKind.Dedicated:
								this.sameTaskTerminals[key] = terminal!.id.toString();
								break;
							case PanelKind.Shared:
								this.idleTaskTerminals.set(key, terminal!.id.toString(), Touch.AsOld);
								break;
						}
D
Dirk Baeumer 已提交
677
					}
678
					let reveal = task.command.presentation!.reveal;
679 680
					let revealProblems = task.command.presentation!.revealProblems;
					let revealProblemPanel = terminal && (revealProblems === RevealProblemKind.OnProblem) && (startStopProblemMatcher.numberOfMatches > 0);
681 682 683
					if (revealProblemPanel) {
						this.panelService.openPanel(Constants.MARKERS_PANEL_ID);
					} else if (terminal && (reveal === RevealKind.Silent) && ((exitCode !== 0) || (startStopProblemMatcher.numberOfMatches > 0) && startStopProblemMatcher.maxMarkerSeverity &&
684
						(startStopProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error))) {
685 686 687
						this.terminalService.setActiveInstance(terminal);
						this.terminalService.showPanel(false);
					}
D
Dirk Baeumer 已提交
688 689
					startStopProblemMatcher.done();
					startStopProblemMatcher.dispose();
690 691 692 693 694 695 696
					registeredLinkMatchers.forEach(handle => {
						if (terminal) {
							terminal.deregisterLinkMatcher(handle);
						}
					});
					if (!processStartedSignaled && terminal) {
						this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal.processId!));
697
						processStartedSignaled = true;
698
					}
A
Alex Ross 已提交
699 700

					this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode));
701
					this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Inactive, task));
702
					this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task));
D
Dirk Baeumer 已提交
703 704 705 706
					resolve({ exitCode });
				});
			});
		}
707

708
		let showProblemPanel = task.command.presentation && (task.command.presentation.revealProblems === RevealProblemKind.Always);
709 710 711
		if (showProblemPanel) {
			this.panelService.openPanel(Constants.MARKERS_PANEL_ID);
		} else if (task.command.presentation && (task.command.presentation.reveal === RevealKind.Always)) {
712
			this.terminalService.setActiveInstance(terminal);
713
			this.terminalService.showPanel(task.command.presentation.focus);
D
Dirk Baeumer 已提交
714
		}
A
Alex Ross 已提交
715
		this.activeTasks[task.getMapKey()] = { terminal, task, promise };
716
		this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Changed));
D
Dirk Baeumer 已提交
717 718 719 720
		return promise.then((summary) => {
			try {
				let telemetryEvent: TelemetryEvent = {
					trigger: trigger,
721
					runner: 'terminal',
A
Alex Ross 已提交
722
					taskKind: task.getTelemetryKind(),
723
					command: this.getSanitizedCommand(executedCommand!),
D
Dirk Baeumer 已提交
724 725 726
					success: true,
					exitCode: summary.exitCode
				};
K
kieferrm 已提交
727
				/* __GDPR__
K
kieferrm 已提交
728 729 730 731
					"taskService" : {
						"${include}": [
							"${TelemetryEvent}"
						]
K
kieferrm 已提交
732
					}
K
kieferrm 已提交
733
				*/
D
Dirk Baeumer 已提交
734 735
				this.telemetryService.publicLog(TerminalTaskSystem.TelemetryEventName, telemetryEvent);
			} catch (error) {
736
			}
D
Dirk Baeumer 已提交
737 738 739 740 741
			return summary;
		}, (error) => {
			try {
				let telemetryEvent: TelemetryEvent = {
					trigger: trigger,
742
					runner: 'terminal',
A
Alex Ross 已提交
743
					taskKind: task.getTelemetryKind(),
744
					command: this.getSanitizedCommand(executedCommand!),
D
Dirk Baeumer 已提交
745 746
					success: false
				};
K
kieferrm 已提交
747
				/* __GDPR__
K
kieferrm 已提交
748 749 750 751 752 753
					"taskService" : {
						"${include}": [
							"${TelemetryEvent}"
						]
					}
				*/
D
Dirk Baeumer 已提交
754 755
				this.telemetryService.publicLog(TerminalTaskSystem.TelemetryEventName, telemetryEvent);
			} catch (error) {
756
			}
757
			return Promise.reject<ITaskSummary>(error);
D
Dirk Baeumer 已提交
758
		});
759 760
	}

761
	private createTerminalName(task: CustomTask | ContributedTask): string {
762
		const needsFolderQualification = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE;
763 764 765
		return nls.localize('TerminalTaskSystem.terminalName', 'Task - {0}', needsFolderQualification ? task.getQualifiedLabel() : task.configurationProperties.name);
	}

A
Alex Ross 已提交
766 767 768 769 770 771 772 773
	private async getUserHome(): Promise<URI> {
		const env = await this.remoteAgentService.getEnvironment();
		if (env) {
			return env.userHome;
		}
		return URI.from({ scheme: Schemas.file, path: this.environmentService.userHome });
	}

774
	private async createShellLaunchConfig(task: CustomTask | ContributedTask, workspaceFolder: IWorkspaceFolder | undefined, variableResolver: VariableResolver, platform: Platform.Platform, options: CommandOptions, command: CommandString, args: CommandString[], waitOnExit: boolean | string): Promise<IShellLaunchConfig | undefined> {
775
		let shellLaunchConfig: IShellLaunchConfig;
776
		let isShellCommand = task.command.runtime === RuntimeType.Shell;
777
		let needsFolderQualification = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE;
778
		let terminalName = this.createTerminalName(task);
A
Alex Ross 已提交
779
		let originalCommand = task.command.name;
D
Dirk Baeumer 已提交
780
		if (isShellCommand) {
781
			const defaultConfig = await this.terminalInstanceService.getDefaultShellAndArgs(true, platform);
D
Daniel Imms 已提交
782
			shellLaunchConfig = { name: terminalName, executable: defaultConfig.shell, args: defaultConfig.args, waitOnExit };
783
			let shellSpecified: boolean = false;
784
			let shellOptions: ShellConfiguration | undefined = task.command.options && task.command.options.shell;
785 786
			if (shellOptions) {
				if (shellOptions.executable) {
A
Alex Ross 已提交
787
					shellLaunchConfig.executable = this.resolveVariable(variableResolver, shellOptions.executable);
788 789
					shellSpecified = true;
				}
D
Dirk Baeumer 已提交
790
				if (shellOptions.args) {
A
Alex Ross 已提交
791
					shellLaunchConfig.args = this.resolveVariables(variableResolver, shellOptions.args.slice());
792
				} else {
793
					shellLaunchConfig.args = [];
794 795
				}
			}
796
			let shellArgs = Array.isArray(shellLaunchConfig.args!) ? <string[]>shellLaunchConfig.args!.slice(0) : [shellLaunchConfig.args!];
797
			let toAdd: string[] = [];
798
			let commandLine = this.buildShellCommandLine(platform, shellLaunchConfig.executable!, shellOptions, command, originalCommand, args);
799
			let windowsShellArgs: boolean = false;
800
			if (platform === Platform.Platform.Windows) {
801
				windowsShellArgs = true;
802
				let basename = path.basename(shellLaunchConfig.executable!).toLowerCase();
A
Alex Ross 已提交
803 804 805
				// If we don't have a cwd, then the terminal uses the home dir.
				const userHome = await this.getUserHome();
				if (basename === 'cmd.exe' && ((options.cwd && isUNC(options.cwd)) || (!options.cwd && isUNC(userHome.fsPath)))) {
A
Alex Ross 已提交
806
					return undefined;
807
				}
A
Alex Ross 已提交
808
				if ((basename === 'powershell.exe') || (basename === 'pwsh.exe')) {
809 810 811
					if (!shellSpecified) {
						toAdd.push('-Command');
					}
812
				} else if ((basename === 'bash.exe') || (basename === 'zsh.exe')) {
813
					windowsShellArgs = false;
814 815 816
					if (!shellSpecified) {
						toAdd.push('-c');
					}
A
Alex Ross 已提交
817
				} else if (basename === 'wsl.exe') {
818
					if (!shellSpecified) {
A
Alex Ross 已提交
819 820
						toAdd.push('-e');
					}
821
				} else {
822 823 824
					if (!shellSpecified) {
						toAdd.push('/d', '/c');
					}
825 826
				}
			} else {
827
				if (!shellSpecified) {
D
Dirk Baeumer 已提交
828
					// Under Mac remove -l to not start it as a login shell.
829
					if (platform === Platform.Platform.Mac) {
D
Dirk Baeumer 已提交
830 831 832 833 834
						let index = shellArgs.indexOf('-l');
						if (index !== -1) {
							shellArgs.splice(index, 1);
						}
					}
835 836
					toAdd.push('-c');
				}
837 838 839 840 841 842 843
			}
			toAdd.forEach(element => {
				if (!shellArgs.some(arg => arg.toLowerCase() === element)) {
					shellArgs.push(element);
				}
			});
			shellArgs.push(commandLine);
844
			shellLaunchConfig.args = windowsShellArgs ? shellArgs.join(' ') : shellArgs;
845
			if (task.command.presentation && task.command.presentation.echo) {
846
				if (needsFolderQualification && workspaceFolder) {
847
					shellLaunchConfig.initialText = `\x1b[1m> Executing task in folder ${workspaceFolder.name}: ${commandLine} <\x1b[0m\n`;
848 849 850
				} else {
					shellLaunchConfig.initialText = `\x1b[1m> Executing task: ${commandLine} <\x1b[0m\n`;
				}
D
Dirk Baeumer 已提交
851
			}
852
		} else {
A
Alex Ross 已提交
853
			let commandExecutable = (task.command.runtime !== RuntimeType.CustomExecution2) ? CommandString.value(command) : undefined;
854
			let executable = !isShellCommand
A
Alex Ross 已提交
855
				? this.resolveVariable(variableResolver, '${' + TerminalTaskSystem.ProcessVarName + '}')
856
				: commandExecutable;
857 858

			// When we have a process task there is no need to quote arguments. So we go ahead and take the string value.
D
Dirk Baeumer 已提交
859
			shellLaunchConfig = {
860
				name: terminalName,
861
				executable: executable,
862
				args: args.map(a => Types.isString(a) ? a : a.value),
863 864
				waitOnExit
			};
865 866
			if (task.command.presentation && task.command.presentation.echo) {
				let getArgsToEcho = (args: string | string[] | undefined): string => {
D
Dirk Baeumer 已提交
867 868 869 870 871 872 873 874
					if (!args || args.length === 0) {
						return '';
					}
					if (Types.isString(args)) {
						return args;
					}
					return args.join(' ');
				};
875
				if (needsFolderQualification && workspaceFolder) {
876
					shellLaunchConfig.initialText = `\x1b[1m> Executing task in folder ${workspaceFolder.name}: ${shellLaunchConfig.executable} ${getArgsToEcho(shellLaunchConfig.args)} <\x1b[0m\n`;
877 878 879
				} else {
					shellLaunchConfig.initialText = `\x1b[1m> Executing task: ${shellLaunchConfig.executable} ${getArgsToEcho(shellLaunchConfig.args)} <\x1b[0m\n`;
				}
D
Dirk Baeumer 已提交
880
			}
881
		}
A
Alex Ross 已提交
882

883
		if (options.cwd) {
884
			let cwd = options.cwd;
B
Benjamin Pasero 已提交
885
			if (!path.isAbsolute(cwd)) {
886
				if (workspaceFolder && (workspaceFolder.uri.scheme === 'file')) {
B
Benjamin Pasero 已提交
887
					cwd = path.join(workspaceFolder.uri.fsPath, cwd);
888 889 890
				}
			}
			// This must be normalized to the OS
A
Alex Ross 已提交
891
			shellLaunchConfig.cwd = resources.toLocalResource(URI.from({ scheme: Schemas.file, path: cwd }), this.environmentService.configuration.remoteAuthority);
892 893
		}
		if (options.env) {
894
			shellLaunchConfig.env = options.env;
895
		}
A
Alex Ross 已提交
896 897 898
		return shellLaunchConfig;
	}

899
	private async createTerminal(task: CustomTask | ContributedTask, resolver: VariableResolver, workspaceFolder: IWorkspaceFolder | undefined): Promise<[ITerminalInstance | undefined, string | undefined, TaskError | undefined]> {
A
Alex Ross 已提交
900 901
		let platform = resolver.taskSystemInfo ? resolver.taskSystemInfo.platform : Platform.platform;
		let options = this.resolveOptions(resolver, task.command.options);
902

A
Alex Ross 已提交
903
		let waitOnExit: boolean | string = false;
904 905 906 907
		const presentationOptions = task.command.presentation;
		if (!presentationOptions) {
			throw new Error('Task presentation options should not be undefined here.');
		}
A
Alex Ross 已提交
908

909 910
		if (presentationOptions.reveal !== RevealKind.Never || !task.configurationProperties.isBackground) {
			if (presentationOptions.panel === PanelKind.New) {
A
Alex Ross 已提交
911
				waitOnExit = nls.localize('closeTerminal', 'Press any key to close the terminal.');
912
			} else if (presentationOptions.showReuseMessage) {
A
Alex Ross 已提交
913 914 915 916 917
				waitOnExit = nls.localize('reuseTerminal', 'Terminal will be reused by tasks, press any key to close it.');
			} else {
				waitOnExit = true;
			}
		}
918 919 920 921

		let commandExecutable: string | undefined;
		let command: CommandString | undefined;
		let args: CommandString[] | undefined;
922
		let launchConfigs: IShellLaunchConfig | undefined;
923

A
Alex Ross 已提交
924
		if (task.command.runtime === RuntimeType.CustomExecution2) {
925
			this.currentTask.shellLaunchConfig = launchConfigs = {
926
				isExtensionTerminal: true,
927
				waitOnExit,
928
				name: this.createTerminalName(task),
929 930
				initialText: task.command.presentation && task.command.presentation.echo ? `\x1b[1m> Executing task: ${task._label} <\x1b[0m\n` : undefined
			};
931
		} else {
932 933 934 935 936
			let resolvedResult: { command: CommandString, args: CommandString[] } = this.resolveCommandAndArgs(resolver, task.command);
			command = resolvedResult.command;
			args = resolvedResult.args;
			commandExecutable = CommandString.value(command);

937
			this.currentTask.shellLaunchConfig = launchConfigs = (this.isRerun && this.lastTask) ? this.lastTask.getVerifiedTask().shellLaunchConfig : await this.createShellLaunchConfig(task, workspaceFolder, resolver, platform, options, command, args, waitOnExit);
938
			if (launchConfigs === undefined) {
939 940
				return [undefined, undefined, new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive using cmd.exe.'), TaskErrors.UnknownError)];
			}
A
Alex Ross 已提交
941
		}
942

943 944
		let prefersSameTerminal = presentationOptions.panel === PanelKind.Dedicated;
		let allowsSharedTerminal = presentationOptions.panel === PanelKind.Shared;
945
		let group = presentationOptions.group;
946

A
Alex Ross 已提交
947
		let taskKey = task.getMapKey();
948
		let terminalToReuse: TerminalData | undefined;
949
		if (prefersSameTerminal) {
950
			let terminalId = this.sameTaskTerminals[taskKey];
951 952
			if (terminalId) {
				terminalToReuse = this.terminals[terminalId];
953
				delete this.sameTaskTerminals[taskKey];
D
Dirk Baeumer 已提交
954
			}
955
		} else if (allowsSharedTerminal) {
956 957 958 959 960 961 962 963
			// Always allow to reuse the terminal previously used by the same task.
			let terminalId = this.idleTaskTerminals.remove(taskKey);
			if (!terminalId) {
				// There is no idle terminal which was used by the same task.
				// Search for any idle terminal used previously by a task of the same group
				// (or, if the task has no group, a terminal used by a task without group).
				for (const taskId of this.idleTaskTerminals.keys()) {
					const idleTerminalId = this.idleTaskTerminals.get(taskId)!;
964
					if (idleTerminalId && this.terminals[idleTerminalId] && this.terminals[idleTerminalId].group === group) {
965 966 967 968 969
						terminalId = this.idleTaskTerminals.remove(taskId);
						break;
					}
				}
			}
970 971
			if (terminalId) {
				terminalToReuse = this.terminals[terminalId];
972
			}
D
Dirk Baeumer 已提交
973
		}
974
		if (terminalToReuse) {
975
			if (!launchConfigs) {
976
				throw new Error('Task shell launch configuration should not be undefined here.');
977 978
			}

979
			terminalToReuse.terminal.reuseTerminal(launchConfigs);
980

981
			if (task.command.presentation && task.command.presentation.clear) {
982 983
				terminalToReuse.terminal.clear();
			}
984
			this.terminals[terminalToReuse.terminal.id.toString()].lastTask = taskKey;
985
			return [terminalToReuse.terminal, commandExecutable, undefined];
986 987
		}

988 989 990 991 992 993 994
		let result: ITerminalInstance | null = null;
		if (group) {
			// Try to find an existing terminal to split.
			// Even if an existing terminal is found, the split can fail if the terminal width is too small.
			for (const terminal of values(this.terminals)) {
				if (terminal.group === group) {
					const originalInstance = terminal.terminal;
995
					await originalInstance.waitForTitle();
996
					result = this.terminalService.splitInstance(originalInstance, launchConfigs);
997 998 999 1000 1001 1002 1003 1004
					if (result) {
						break;
					}
				}
			}
		}
		if (!result) {
			// Either no group is used, no terminal with the group exists or splitting an existing terminal failed.
1005
			result = this.terminalService.createTerminal(launchConfigs);
1006 1007
		}

1008
		const terminalKey = result.id.toString();
D
Dirk Baeumer 已提交
1009
		result.onDisposed((terminal) => {
1010
			let terminalData = this.terminals[terminalKey];
D
Dirk Baeumer 已提交
1011
			if (terminalData) {
1012
				delete this.terminals[terminalKey];
1013 1014
				delete this.sameTaskTerminals[terminalData.lastTask];
				this.idleTaskTerminals.delete(terminalData.lastTask);
1015 1016 1017 1018
				// Delete the task now as a work around for cases when the onExit isn't fired.
				// This can happen if the terminal wasn't shutdown with an "immediate" flag and is expected.
				// For correct terminal re-use, the task needs to be deleted immediately.
				// Note that this shouldn't be a problem anymore since user initiated terminal kills are now immediate.
A
Alex Ross 已提交
1019
				delete this.activeTasks[task.getMapKey()];
D
Dirk Baeumer 已提交
1020 1021
			}
		});
1022
		this.terminals[terminalKey] = { terminal: result, lastTask: taskKey, group };
1023
		return [result, commandExecutable, undefined];
1024 1025
	}

1026
	private buildShellCommandLine(platform: Platform.Platform, shellExecutable: string, shellOptions: ShellConfiguration | undefined, command: CommandString, originalCommand: CommandString | undefined, args: CommandString[]): string {
1027
		let basename = path.parse(shellExecutable).name.toLowerCase();
A
Alex Ross 已提交
1028
		let shellQuoteOptions = this.getQuotingOptions(basename, shellOptions, platform);
1029 1030 1031 1032 1033 1034 1035 1036

		function needsQuotes(value: string): boolean {
			if (value.length >= 2) {
				let first = value[0] === shellQuoteOptions.strong ? shellQuoteOptions.strong : value[0] === shellQuoteOptions.weak ? shellQuoteOptions.weak : undefined;
				if (first === value[value.length - 1]) {
					return false;
				}
			}
1037
			let quote: string | undefined;
1038
			for (let i = 0; i < value.length; i++) {
1039 1040 1041 1042
				// We found the end quote.
				let ch = value[i];
				if (ch === quote) {
					quote = undefined;
R
Rob Lourens 已提交
1043
				} else if (quote !== undefined) {
1044 1045 1046 1047 1048 1049 1050 1051
					// skip the character. We are quoted.
					continue;
				} else if (ch === shellQuoteOptions.escape) {
					// Skip the next character
					i++;
				} else if (ch === shellQuoteOptions.strong || ch === shellQuoteOptions.weak) {
					quote = ch;
				} else if (ch === ' ') {
1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090
					return true;
				}
			}
			return false;
		}

		function quote(value: string, kind: ShellQuoting): [string, boolean] {
			if (kind === ShellQuoting.Strong && shellQuoteOptions.strong) {
				return [shellQuoteOptions.strong + value + shellQuoteOptions.strong, true];
			} else if (kind === ShellQuoting.Weak && shellQuoteOptions.weak) {
				return [shellQuoteOptions.weak + value + shellQuoteOptions.weak, true];
			} else if (kind === ShellQuoting.Escape && shellQuoteOptions.escape) {
				if (Types.isString(shellQuoteOptions.escape)) {
					return [value.replace(/ /g, shellQuoteOptions.escape + ' '), true];
				} else {
					let buffer: string[] = [];
					for (let ch of shellQuoteOptions.escape.charsToEscape) {
						buffer.push(`\\${ch}`);
					}
					let regexp: RegExp = new RegExp('[' + buffer.join(',') + ']', 'g');
					let escapeChar = shellQuoteOptions.escape.escapeChar;
					return [value.replace(regexp, (match) => escapeChar + match), true];
				}
			}
			return [value, false];
		}

		function quoteIfNecessary(value: CommandString): [string, boolean] {
			if (Types.isString(value)) {
				if (needsQuotes(value)) {
					return quote(value, ShellQuoting.Strong);
				} else {
					return [value, false];
				}
			} else {
				return quote(value.value, value.quoting);
			}
		}

1091 1092 1093 1094 1095 1096 1097
		// If we have no args and the command is a string then use the command to stay backwards compatible with the old command line
		// model. To allow variable resolving with spaces we do continue if the resolved value is different than the original one
		// and the resolved one needs quoting.
		if ((!args || args.length === 0) && Types.isString(command) && (command === originalCommand as string || needsQuotes(originalCommand as string))) {
			return command;
		}

1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113
		let result: string[] = [];
		let commandQuoted = false;
		let argQuoted = false;
		let value: string;
		let quoted: boolean;
		[value, quoted] = quoteIfNecessary(command);
		result.push(value);
		commandQuoted = quoted;
		for (let arg of args) {
			[value, quoted] = quoteIfNecessary(arg);
			result.push(value);
			argQuoted = argQuoted || quoted;
		}

		let commandLine = result.join(' ');
		// There are special rules quoted command line in cmd.exe
1114
		if (platform === Platform.Platform.Windows) {
1115 1116 1117 1118 1119 1120 1121
			if (basename === 'cmd' && commandQuoted && argQuoted) {
				commandLine = '"' + commandLine + '"';
			} else if (basename === 'powershell' && commandQuoted) {
				commandLine = '& ' + commandLine;
			}
		}

1122
		if (basename === 'cmd' && platform === Platform.Platform.Windows && commandQuoted && argQuoted) {
1123 1124 1125
			commandLine = '"' + commandLine + '"';
		}
		return commandLine;
1126 1127
	}

A
Alex Ross 已提交
1128
	private getQuotingOptions(shellBasename: string, shellOptions: ShellConfiguration | undefined, platform: Platform.Platform): ShellQuotingOptions {
1129 1130 1131
		if (shellOptions && shellOptions.quoting) {
			return shellOptions.quoting;
		}
A
Alex Ross 已提交
1132
		return TerminalTaskSystem.shellQuotes[shellBasename] || TerminalTaskSystem.osShellQuotes[Platform.PlatformToString(platform)];
1133 1134
	}

1135
	private collectTaskVariables(variables: Set<string>, task: CustomTask | ContributedTask): void {
A
Alex Ross 已提交
1136
		if (task.command && task.command.name) {
1137
			this.collectCommandVariables(variables, task.command, task);
1138
		}
A
Alex Ross 已提交
1139
		this.collectMatcherVariables(variables, task.configurationProperties.problemMatchers);
1140 1141
	}

1142
	private collectCommandVariables(variables: Set<string>, command: CommandConfiguration, task: CustomTask | ContributedTask): void {
1143
		// The custom execution should have everything it needs already as it provided
1144
		// the callback.
A
Alex Ross 已提交
1145
		if (command.runtime === RuntimeType.CustomExecution2) {
1146 1147 1148
			return;
		}

R
Rob Lourens 已提交
1149
		if (command.name === undefined) {
1150 1151
			throw new Error('Command name should never be undefined here.');
		}
1152 1153 1154 1155
		this.collectVariables(variables, command.name);
		if (command.args) {
			command.args.forEach(arg => this.collectVariables(variables, arg));
		}
1156 1157 1158 1159 1160
		// Try to get a scope.
		const scope = (<ExtensionTaskSource>task._source).scope;
		if (scope !== TaskScope.Global) {
			variables.add('${workspaceFolder}');
		}
1161 1162 1163 1164 1165
		if (command.options) {
			let options = command.options;
			if (options.cwd) {
				this.collectVariables(variables, options.cwd);
			}
1166 1167 1168 1169
			const optionsEnv = options.env;
			if (optionsEnv) {
				Object.keys(optionsEnv).forEach((key) => {
					let value: any = optionsEnv[key];
1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185
					if (Types.isString(value)) {
						this.collectVariables(variables, value);
					}
				});
			}
			if (options.shell) {
				if (options.shell.executable) {
					this.collectVariables(variables, options.shell.executable);
				}
				if (options.shell.args) {
					options.shell.args.forEach(arg => this.collectVariables(variables, arg));
				}
			}
		}
	}

1186
	private collectMatcherVariables(variables: Set<string>, values: Array<string | ProblemMatcher> | undefined): void {
R
Rob Lourens 已提交
1187
		if (values === undefined || values === null || values.length === 0) {
1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209
			return;
		}
		values.forEach((value) => {
			let matcher: ProblemMatcher;
			if (Types.isString(value)) {
				if (value[0] === '$') {
					matcher = ProblemMatcherRegistry.get(value.substring(1));
				} else {
					matcher = ProblemMatcherRegistry.get(value);
				}
			} else {
				matcher = value;
			}
			if (matcher && matcher.filePrefix) {
				this.collectVariables(variables, matcher.filePrefix);
			}
		});
	}

	private collectVariables(variables: Set<string>, value: string | CommandString): void {
		let string: string = Types.isString(value) ? value : value.value;
		let r = /\$\{(.*?)\}/g;
M
Matt Bierner 已提交
1210
		let matches: RegExpExecArray | null;
1211 1212 1213 1214 1215 1216 1217 1218 1219
		do {
			matches = r.exec(string);
			if (matches) {
				variables.add(matches[0]);
			}
		} while (matches);
	}

	private resolveCommandAndArgs(resolver: VariableResolver, commandConfig: CommandConfiguration): { command: CommandString, args: CommandString[] } {
1220
		// First we need to use the command args:
1221 1222 1223
		let args: CommandString[] = commandConfig.args ? commandConfig.args.slice() : [];
		args = this.resolveVariables(resolver, args);
		let command: CommandString = this.resolveVariable(resolver, commandConfig.name);
1224 1225 1226
		return { command, args };
	}

1227 1228 1229 1230
	private resolveVariables(resolver: VariableResolver, value: string[]): string[];
	private resolveVariables(resolver: VariableResolver, value: CommandString[]): CommandString[];
	private resolveVariables(resolver: VariableResolver, value: CommandString[]): CommandString[] {
		return value.map(s => this.resolveVariable(resolver, s));
1231 1232
	}

1233
	private resolveMatchers(resolver: VariableResolver, values: Array<string | ProblemMatcher> | undefined): ProblemMatcher[] {
R
Rob Lourens 已提交
1234
		if (values === undefined || values === null || values.length === 0) {
1235 1236
			return [];
		}
1237 1238 1239 1240
		let result: ProblemMatcher[] = [];
		values.forEach((value) => {
			let matcher: ProblemMatcher;
			if (Types.isString(value)) {
1241 1242 1243 1244 1245
				if (value[0] === '$') {
					matcher = ProblemMatcherRegistry.get(value.substring(1));
				} else {
					matcher = ProblemMatcherRegistry.get(value);
				}
1246 1247 1248 1249
			} else {
				matcher = value;
			}
			if (!matcher) {
A
Alex Ross 已提交
1250
				this.appendOutput(nls.localize('unknownProblemMatcher', 'Problem matcher {0} can\'t be resolved. The matcher will be ignored'));
1251 1252
				return;
			}
1253
			let taskSystemInfo: TaskSystemInfo | undefined = resolver.taskSystemInfo;
R
Rob Lourens 已提交
1254 1255
			let hasFilePrefix = matcher.filePrefix !== undefined;
			let hasUriProvider = taskSystemInfo !== undefined && taskSystemInfo.uriProvider !== undefined;
1256
			if (!hasFilePrefix && !hasUriProvider) {
1257 1258
				result.push(matcher);
			} else {
J
Johannes Rieken 已提交
1259
				let copy = Objects.deepClone(matcher);
R
Rob Lourens 已提交
1260
				if (hasUriProvider && (taskSystemInfo !== undefined)) {
1261
					copy.uriProvider = taskSystemInfo.uriProvider;
1262 1263
				}
				if (hasFilePrefix) {
1264
					copy.filePrefix = this.resolveVariable(resolver, copy.filePrefix);
1265
				}
1266 1267 1268 1269 1270 1271
				result.push(copy);
			}
		});
		return result;
	}

1272 1273 1274
	private resolveVariable(resolver: VariableResolver, value: string | undefined): string;
	private resolveVariable(resolver: VariableResolver, value: CommandString | undefined): CommandString;
	private resolveVariable(resolver: VariableResolver, value: CommandString | undefined): CommandString {
1275
		// TODO@Dirk Task.getWorkspaceFolder should return a WorkspaceFolder that is defined in workspace.ts
1276
		if (Types.isString(value)) {
1277
			return resolver.resolve(value);
R
Rob Lourens 已提交
1278
		} else if (value !== undefined) {
1279
			return {
1280
				value: resolver.resolve(value.value),
1281 1282
				quoting: value.quoting
			};
1283 1284
		} else { // This should never happen
			throw new Error('Should never try to resolve undefined.');
1285
		}
1286 1287
	}

1288
	private resolveOptions(resolver: VariableResolver, options: CommandOptions | undefined): CommandOptions {
R
Rob Lourens 已提交
1289
		if (options === undefined || options === null) {
1290
			return { cwd: this.resolveVariable(resolver, '${workspaceFolder}') };
1291 1292
		}
		let result: CommandOptions = Types.isString(options.cwd)
1293 1294
			? { cwd: this.resolveVariable(resolver, options.cwd) }
			: { cwd: this.resolveVariable(resolver, '${workspaceFolder}') };
1295 1296 1297
		if (options.env) {
			result.env = Object.create(null);
			Object.keys(options.env).forEach((key) => {
1298
				let value: any = options.env![key];
1299
				if (Types.isString(value)) {
1300
					result.env![key] = this.resolveVariable(resolver, value);
1301
				} else {
1302
					result.env![key] = value.toString();
1303 1304 1305 1306
				}
			});
		}
		return result;
1307 1308
	}

D
Dirk Baeumer 已提交
1309
	private registerLinkMatchers(terminal: ITerminalInstance, problemMatchers: ProblemMatcher[]): number[] {
1310
		let result: number[] = [];
D
Dirk Baeumer 已提交
1311
		/*
1312 1313 1314 1315 1316 1317 1318 1319 1320
		let handlePattern = (matcher: ProblemMatcher, pattern: ProblemPattern): void => {
			if (pattern.regexp instanceof RegExp && Types.isNumber(pattern.file)) {
				result.push(terminal.registerLinkMatcher(pattern.regexp, (match: string) => {
					let resource: URI = getResource(match, matcher);
					if (resource) {
						this.workbenchEditorService.openEditor({
							resource: resource
						});
					}
D
Dirk Baeumer 已提交
1321
				}, 0));
1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333
			}
		};

		for (let problemMatcher of problemMatchers) {
			if (Array.isArray(problemMatcher.pattern)) {
				for (let pattern of problemMatcher.pattern) {
					handlePattern(problemMatcher, pattern);
				}
			} else if (problemMatcher.pattern) {
				handlePattern(problemMatcher, problemMatcher.pattern);
			}
		}
D
Dirk Baeumer 已提交
1334
		*/
1335
		return result;
1336 1337
	}

1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357
	private static WellKnowCommands: IStringDictionary<boolean> = {
		'ant': true,
		'cmake': true,
		'eslint': true,
		'gradle': true,
		'grunt': true,
		'gulp': true,
		'jake': true,
		'jenkins': true,
		'jshint': true,
		'make': true,
		'maven': true,
		'msbuild': true,
		'msc': true,
		'nmake': true,
		'npm': true,
		'rake': true,
		'tsc': true,
		'xbuild': true
	};
1358

1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369
	public getSanitizedCommand(cmd: string): string {
		let result = cmd.toLowerCase();
		let index = result.lastIndexOf(path.sep);
		if (index !== -1) {
			result = result.substring(index + 1);
		}
		if (TerminalTaskSystem.WellKnowCommands[result]) {
			return result;
		}
		return 'other';
	}
1370 1371 1372 1373 1374 1375 1376

	private appendOutput(output: string): void {
		const outputChannel = this.outputService.getChannel(this.outputChannelId);
		if (outputChannel) {
			outputChannel.append(output);
		}
	}
1377 1378 1379 1380 1381 1382 1383

	private async findExecutable(command: string, cwd?: string, paths?: string[]): Promise<string> {
		// If we have an absolute path then we take it.
		if (path.isAbsolute(command)) {
			return command;
		}
		if (cwd === undefined) {
1384
			cwd = processCwd();
1385 1386 1387 1388 1389 1390 1391
		}
		const dir = path.dirname(command);
		if (dir !== '.') {
			// We have a directory and the directory is relative (see above). Make the path absolute
			// to the current working directory.
			return path.join(cwd, command);
		}
1392 1393
		if (paths === undefined && Types.isString(processEnv.PATH)) {
			paths = processEnv.PATH.split(path.delimiter);
1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423
		}
		// No PATH environment. Make path absolute to the cwd.
		if (paths === undefined || paths.length === 0) {
			return path.join(cwd, command);
		}
		// We have a simple file name. We get the path variable from the env
		// and try to find the executable on the path.
		for (let pathEntry of paths) {
			// The path entry is absolute.
			let fullPath: string;
			if (path.isAbsolute(pathEntry)) {
				fullPath = path.join(pathEntry, command);
			} else {
				fullPath = path.join(cwd, pathEntry, command);
			}

			if (await this.fileService.exists(resources.toLocalResource(URI.from({ scheme: Schemas.file, path: fullPath }), this.environmentService.configuration.remoteAuthority))) {
				return fullPath;
			}
			let withExtension = fullPath + '.com';
			if (await this.fileService.exists(resources.toLocalResource(URI.from({ scheme: Schemas.file, path: withExtension }), this.environmentService.configuration.remoteAuthority))) {
				return withExtension;
			}
			withExtension = fullPath + '.exe';
			if (await this.fileService.exists(resources.toLocalResource(URI.from({ scheme: Schemas.file, path: withExtension }), this.environmentService.configuration.remoteAuthority))) {
				return withExtension;
			}
		}
		return path.join(cwd, command);
	}
J
Johannes Rieken 已提交
1424
}