Files
vscode/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts
T

1502 lines
58 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import * as nls from 'vs/nls';
import * as Objects from 'vs/base/common/objects';
import * as Types from 'vs/base/common/types';
import * as Platform from 'vs/base/common/platform';
import * as Async from 'vs/base/common/async';
import * as resources from 'vs/base/common/resources';
import { IStringDictionary, values } from 'vs/base/common/collections';
import { LinkedMap, Touch } from 'vs/base/common/map';
import Severity from 'vs/base/common/severity';
import { Event, Emitter } from 'vs/base/common/event';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { isUNC } from 'vs/base/common/extpath';
import { IFileService } from 'vs/platform/files/common/files';
import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers';
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ProblemMatcher, ProblemMatcherRegistry /*, ProblemPattern, getResource */ } from 'vs/workbench/contrib/tasks/common/problemMatcher';
import Constants from 'vs/workbench/contrib/markers/browser/constants';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IShellLaunchConfig } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITerminalService, ITerminalInstanceService, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IOutputService } from 'vs/workbench/contrib/output/common/output';
import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEventKind, ProblemHandlingStrategy } from 'vs/workbench/contrib/tasks/common/problemCollectors';
import {
Task, CustomTask, ContributedTask, RevealKind, CommandOptions, ShellConfiguration, RuntimeType, PanelKind,
TaskEvent, TaskEventKind, ShellQuotingOptions, ShellQuoting, CommandString, CommandConfiguration, ExtensionTaskSource, TaskScope, RevealProblemKind, DependsOrder
} from 'vs/workbench/contrib/tasks/common/tasks';
import {
ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, ITaskResolver,
TelemetryEvent, Triggers, TaskTerminateResponse, TaskSystemInfoResolver, TaskSystemInfo, ResolveSet, ResolvedVariables
} from 'vs/workbench/contrib/tasks/common/taskSystem';
import { URI } from 'vs/base/common/uri';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { Schemas } from 'vs/base/common/network';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService';
import { env as processEnv, cwd as processCwd } from 'vs/base/common/process';
interface TerminalData {
terminal: ITerminalInstance;
lastTask: string;
group?: string;
}
interface ActiveTerminalData {
terminal: ITerminalInstance;
task: Task;
promise: Promise<ITaskSummary>;
}
class VariableResolver {
constructor(public workspaceFolder: IWorkspaceFolder | undefined, public taskSystemInfo: TaskSystemInfo | undefined, private _values: Map<string, string>, private _service: IConfigurationResolverService | undefined) {
}
resolve(value: string): string {
return value.replace(/\$\{(.*?)\}/g, (match: string, variable: string) => {
// Strip out the ${} because the map contains them variables without those characters.
let result = this._values.get(match.substring(2, match.length - 1));
if ((result !== undefined) && (result !== null)) {
return result;
}
if (this._service) {
return this._service.resolve(this.workspaceFolder, match);
}
return match;
});
}
}
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 {
let verified = false;
if (this.trigger && this.resolvedVariables && this.workspaceFolder && (this.shellLaunchConfig !== undefined)) {
verified = true;
}
return verified;
}
public getVerifiedTask(): { task: Task, resolver: ITaskResolver, trigger: string, resolvedVariables: ResolvedVariables, systemInfo: TaskSystemInfo, workspaceFolder: IWorkspaceFolder, shellLaunchConfig: IShellLaunchConfig } {
if (this.verify()) {
return { task: this.task, resolver: this.resolver, trigger: this.trigger, resolvedVariables: this.resolvedVariables!, systemInfo: this.systemInfo!, workspaceFolder: this.workspaceFolder!, shellLaunchConfig: this.shellLaunchConfig! };
} else {
throw new Error('VerifiedTask was not checked. verify must be checked before getVerifiedTask.');
}
}
}
export class TerminalTaskSystem implements ITaskSystem {
public static TelemetryEventName: string = 'taskService';
private static readonly ProcessVarName = '__process__';
private static shellQuotes: IStringDictionary<ShellQuotingOptions> = {
'cmd': {
strong: '"'
},
'powershell': {
escape: {
escapeChar: '`',
charsToEscape: ' "\'()'
},
strong: '\'',
weak: '"'
},
'bash': {
escape: {
escapeChar: '\\',
charsToEscape: ' "\''
},
strong: '\'',
weak: '"'
},
'zsh': {
escape: {
escapeChar: '\\',
charsToEscape: ' "\''
},
strong: '\'',
weak: '"'
}
};
private static osShellQuotes: IStringDictionary<ShellQuotingOptions> = {
'Linux': TerminalTaskSystem.shellQuotes['bash'],
'Mac': TerminalTaskSystem.shellQuotes['bash'],
'Windows': TerminalTaskSystem.shellQuotes['powershell']
};
private activeTasks: IStringDictionary<ActiveTerminalData>;
private busyTasks: IStringDictionary<Task>;
private terminals: IStringDictionary<TerminalData>;
private idleTaskTerminals: LinkedMap<string, string>;
private sameTaskTerminals: IStringDictionary<string>;
private taskSystemInfoResolver: TaskSystemInfoResolver;
private lastTask: VerifiedTask | undefined;
// Should always be set in run
private currentTask!: VerifiedTask;
private isRerun: boolean = false;
private readonly _onDidStateChange: Emitter<TaskEvent>;
constructor(
private terminalService: ITerminalService,
private outputService: IOutputService,
private panelService: IPanelService,
private markerService: IMarkerService, private modelService: IModelService,
private configurationResolverService: IConfigurationResolverService,
private telemetryService: ITelemetryService,
private contextService: IWorkspaceContextService,
private environmentService: IWorkbenchEnvironmentService,
private outputChannelId: string,
private fileService: IFileService,
private terminalInstanceService: ITerminalInstanceService,
private remotePathService: IRemotePathService,
taskSystemInfoResolver: TaskSystemInfoResolver,
) {
this.activeTasks = Object.create(null);
this.busyTasks = Object.create(null);
this.terminals = Object.create(null);
this.idleTaskTerminals = new LinkedMap<string, string>();
this.sameTaskTerminals = Object.create(null);
this._onDidStateChange = new Emitter();
this.taskSystemInfoResolver = taskSystemInfoResolver;
}
public get onDidStateChange(): Event<TaskEvent> {
return this._onDidStateChange.event;
}
public log(value: string): void {
this.appendOutput(value + '\n');
}
protected showOutput(): void {
this.outputService.showChannel(this.outputChannelId, true);
}
public run(task: Task, resolver: ITaskResolver, trigger: string = Triggers.command): ITaskExecuteResult {
this.currentTask = new VerifiedTask(task, resolver, trigger);
let terminalData = this.activeTasks[task.getMapKey()];
if (terminalData && terminalData.promise) {
let reveal = RevealKind.Always;
let focus = false;
if (CustomTask.is(task) || ContributedTask.is(task)) {
reveal = task.command.presentation!.reveal;
focus = task.command.presentation!.focus;
}
if (reveal === RevealKind.Always || focus) {
this.terminalService.setActiveInstance(terminalData.terminal);
this.terminalService.showPanel(focus);
}
this.lastTask = this.currentTask;
return { kind: TaskExecuteKind.Active, task, active: { same: true, background: task.configurationProperties.isBackground! }, promise: terminalData.promise };
}
try {
const executeResult = { kind: TaskExecuteKind.Started, task, started: {}, promise: this.executeTask(task, resolver, trigger) };
executeResult.promise.then(summary => {
this.lastTask = this.currentTask;
});
return executeResult;
} 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);
}
}
}
public rerun(): ITaskExecuteResult | undefined {
if (this.lastTask && this.lastTask.verify()) {
if ((this.lastTask.task.runOptions.reevaluateOnRerun !== undefined) && !this.lastTask.task.runOptions.reevaluateOnRerun) {
this.isRerun = true;
}
const result = this.run(this.lastTask.task, this.lastTask.resolver);
result.promise.then(summary => {
this.isRerun = false;
});
return result;
} else {
return undefined;
}
}
public revealTask(task: Task): boolean {
let terminalData = this.activeTasks[task.getMapKey()];
if (!terminalData) {
return false;
}
this.terminalService.setActiveInstance(terminalData.terminal);
if (CustomTask.is(task) || ContributedTask.is(task)) {
this.terminalService.showPanel(task.command.presentation!.focus);
}
return true;
}
public isActive(): Promise<boolean> {
return Promise.resolve(this.isActiveSync());
}
public isActiveSync(): boolean {
return Object.keys(this.activeTasks).length > 0;
}
public canAutoTerminate(): boolean {
return Object.keys(this.activeTasks).every(key => !this.activeTasks[key].task.configurationProperties.promptOnClose);
}
public getActiveTasks(): Task[] {
return Object.keys(this.activeTasks).map(key => this.activeTasks[key].task);
}
public getBusyTasks(): Task[] {
return Object.keys(this.busyTasks).map(key => this.busyTasks[key]);
}
public customExecutionComplete(task: Task, result: number): Promise<void> {
let activeTerminal = this.activeTasks[task.getMapKey()];
if (!activeTerminal) {
return Promise.reject(new Error('Expected to have a terminal for an custom execution task'));
}
return new Promise<void>((resolve) => {
// activeTerminal.terminal.rendererExit(result);
resolve();
});
}
public terminate(task: Task): Promise<TaskTerminateResponse> {
let activeTerminal = this.activeTasks[task.getMapKey()];
if (!activeTerminal) {
return Promise.resolve<TaskTerminateResponse>({ success: false, task: undefined });
}
return new Promise<TaskTerminateResponse>((resolve, reject) => {
let terminal = activeTerminal.terminal;
const onExit = terminal.onExit(() => {
let task = activeTerminal.task;
try {
onExit.dispose();
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Terminated, task));
} catch (error) {
// Do nothing.
}
resolve({ success: true, task: task });
});
terminal.dispose();
});
}
public terminateAll(): Promise<TaskTerminateResponse[]> {
let promises: Promise<TaskTerminateResponse>[] = [];
Object.keys(this.activeTasks).forEach((key) => {
let terminalData = this.activeTasks[key];
let terminal = terminalData.terminal;
promises.push(new Promise<TaskTerminateResponse>((resolve, reject) => {
const onExit = terminal.onExit(() => {
let task = terminalData.task;
try {
onExit.dispose();
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Terminated, task));
} catch (error) {
// Do nothing.
}
resolve({ success: true, task: terminalData.task });
});
}));
terminal.dispose();
});
this.activeTasks = Object.create(null);
return Promise.all<TaskTerminateResponse>(promises);
}
private async executeTask(task: Task, resolver: ITaskResolver, trigger: string, alreadyResolved?: Map<string, string>): Promise<ITaskSummary> {
alreadyResolved = alreadyResolved ?? new Map<string, string>();
let promises: Promise<ITaskSummary>[] = [];
if (task.configurationProperties.dependsOn) {
for (const dependency of task.configurationProperties.dependsOn) {
let dependencyTask = resolver.resolve(dependency.uri, dependency.task!);
if (dependencyTask) {
let key = dependencyTask.getMapKey();
let promise = this.activeTasks[key] ? this.activeTasks[key].promise : undefined;
if (!promise) {
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.DependsOnStarted, task));
promise = this.executeTask(dependencyTask, resolver, trigger, alreadyResolved);
}
if (task.configurationProperties.dependsOrder === DependsOrder.sequence) {
promise = Promise.resolve(await promise);
}
promises.push(promise);
} else {
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.uri.toString()
));
this.showOutput();
}
}
}
if ((ContributedTask.is(task) || CustomTask.is(task)) && (task.command)) {
return Promise.all(promises).then((summaries): Promise<ITaskSummary> | ITaskSummary => {
for (let summary of summaries) {
if (summary.exitCode !== 0) {
return { exitCode: summary.exitCode };
}
}
if (this.isRerun) {
return this.reexecuteCommand(task, trigger, alreadyResolved!);
} else {
return this.executeCommand(task, trigger, alreadyResolved!);
}
});
} else {
return Promise.all(promises).then((summaries): ITaskSummary => {
for (let summary of summaries) {
if (summary.exitCode !== 0) {
return { exitCode: summary.exitCode };
}
}
return { exitCode: 0 };
});
}
}
private resolveAndFindExecutable(workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, cwd: string | undefined, envPath: string | undefined): Promise<string> {
return this.findExecutable(
this.configurationResolverService.resolve(workspaceFolder, CommandString.value(task.command.name!)),
cwd ? this.configurationResolverService.resolve(workspaceFolder, cwd) : undefined,
envPath ? envPath.split(path.delimiter).map(p => this.configurationResolverService.resolve(workspaceFolder, p)) : undefined
);
}
private findUnresolvedVariables(variables: Set<string>, alreadyResolved: Map<string, string>): Set<string> {
if (alreadyResolved.size === 0) {
return variables;
}
const unresolved = new Set<string>();
for (const variable of variables) {
if (!alreadyResolved.has(variable.substring(2, variable.length - 1))) {
unresolved.add(variable);
}
}
return unresolved;
}
private mergeMaps(mergeInto: Map<string, string>, mergeFrom: Map<string, string>) {
for (const entry of mergeFrom) {
if (!mergeInto.has(entry[0])) {
mergeInto.set(entry[0], entry[1]);
}
}
}
private resolveVariablesFromSet(taskSystemInfo: TaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, variables: Set<string>, alreadyResolved: Map<string, string>): Promise<ResolvedVariables> {
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;
}
}
}
const unresolved = this.findUnresolvedVariables(variables, alreadyResolved);
let resolvedVariables: Promise<ResolvedVariables>;
if (taskSystemInfo && workspaceFolder) {
let resolveSet: ResolveSet = {
variables: unresolved
};
if (taskSystemInfo.platform === Platform.Platform.Windows && isProcess) {
resolveSet.process = { name: CommandString.value(task.command.name!) };
if (cwd) {
resolveSet.process.cwd = cwd;
}
if (envPath) {
resolveSet.process.path = envPath;
}
}
resolvedVariables = taskSystemInfo.resolveVariables(workspaceFolder, resolveSet).then(async (resolved) => {
this.mergeMaps(alreadyResolved, resolved.variables);
resolved.variables = new Map(alreadyResolved);
if (isProcess) {
let process = CommandString.value(task.command.name!);
if (taskSystemInfo.platform === Platform.Platform.Windows) {
process = await this.resolveAndFindExecutable(workspaceFolder, task, cwd, envPath);
}
resolved.variables.set(TerminalTaskSystem.ProcessVarName, process);
}
return Promise.resolve(resolved);
});
return resolvedVariables;
} else {
let variablesArray = new Array<string>();
unresolved.forEach(variable => variablesArray.push(variable));
return new Promise((resolve, reject) => {
this.configurationResolverService.resolveWithInteraction(workspaceFolder, variablesArray, 'tasks').then(async (resolvedVariablesMap: Map<string, string> | undefined) => {
if (resolvedVariablesMap) {
this.mergeMaps(alreadyResolved, resolvedVariablesMap);
resolvedVariablesMap = new Map(alreadyResolved);
if (isProcess) {
let processVarValue: string;
if (Platform.isWindows) {
processVarValue = await this.resolveAndFindExecutable(workspaceFolder, task, cwd, envPath);
} else {
processVarValue = this.configurationResolverService.resolve(workspaceFolder, CommandString.value(task.command.name!));
}
resolvedVariablesMap.set(TerminalTaskSystem.ProcessVarName, processVarValue);
}
let resolvedVariablesResult: ResolvedVariables = {
variables: resolvedVariablesMap,
};
resolve(resolvedVariablesResult);
} else {
resolve(undefined);
}
}, reason => {
reject(reason);
});
});
}
}
private executeCommand(task: CustomTask | ContributedTask, trigger: string, alreadyResolved: Map<string, string>): Promise<ITaskSummary> {
const taskWorkspaceFolder = task.getWorkspaceFolder();
let workspaceFolder: IWorkspaceFolder | undefined;
if (taskWorkspaceFolder) {
workspaceFolder = this.currentTask.workspaceFolder = taskWorkspaceFolder;
} else {
const folders = this.contextService.getWorkspace().folders;
workspaceFolder = folders.length > 0 ? folders[0] : undefined;
}
const systemInfo: TaskSystemInfo | undefined = this.currentTask.systemInfo = workspaceFolder ? this.taskSystemInfoResolver(workspaceFolder) : undefined;
let variables = new Set<string>();
this.collectTaskVariables(variables, task);
const resolvedVariables = this.resolveVariablesFromSet(systemInfo, workspaceFolder, task, variables, alreadyResolved);
return resolvedVariables.then((resolvedVariables) => {
const isCustomExecution = (task.command.runtime === RuntimeType.CustomExecution);
if (resolvedVariables && (task.command !== undefined) && task.command.runtime && (isCustomExecution || (task.command.name !== undefined))) {
this.currentTask.resolvedVariables = resolvedVariables;
return this.executeInTerminal(task, trigger, new VariableResolver(workspaceFolder, systemInfo, resolvedVariables.variables, this.configurationResolverService), workspaceFolder);
} else {
// Allows the taskExecutions array to be updated in the extension host
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task));
return Promise.resolve({ exitCode: 0 });
}
}, reason => {
return Promise.reject(reason);
});
}
private reexecuteCommand(task: CustomTask | ContributedTask, trigger: string, alreadyResolved: Map<string, string>): Promise<ITaskSummary> {
const lastTask = this.lastTask;
if (!lastTask) {
return Promise.reject(new Error('No task previously run'));
}
const workspaceFolder = this.currentTask.workspaceFolder = lastTask.workspaceFolder;
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 => {
if (value.substring(2, value.length - 1) in lastTask.getVerifiedTask().resolvedVariables) {
hasAllVariables = false;
}
});
if (!hasAllVariables) {
return this.resolveVariablesFromSet(lastTask.getVerifiedTask().systemInfo, lastTask.getVerifiedTask().workspaceFolder, task, variables, alreadyResolved).then((resolvedVariables) => {
this.currentTask.resolvedVariables = resolvedVariables;
return this.executeInTerminal(task, trigger, new VariableResolver(lastTask.getVerifiedTask().workspaceFolder, lastTask.getVerifiedTask().systemInfo, resolvedVariables.variables, this.configurationResolverService), workspaceFolder!);
}, reason => {
return Promise.reject(reason);
});
} else {
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!);
}
}
private async executeInTerminal(task: CustomTask | ContributedTask, trigger: string, resolver: VariableResolver, workspaceFolder: IWorkspaceFolder | undefined): Promise<ITaskSummary> {
let terminal: ITerminalInstance | undefined = undefined;
let executedCommand: string | undefined = undefined;
let error: TaskError | undefined = undefined;
let promise: Promise<ITaskSummary> | undefined = undefined;
if (task.configurationProperties.isBackground) {
const problemMatchers = this.resolveMatchers(resolver, task.configurationProperties.problemMatchers);
let watchingProblemMatcher = new WatchingProblemCollector(problemMatchers, this.markerService, this.modelService, this.fileService);
if ((problemMatchers.length > 0) && !watchingProblemMatcher.isWatching()) {
this.appendOutput(nls.localize('TerminalTaskSystem.nonWatchingMatcher', 'Task {0} is a background task but uses a problem matcher without a background pattern', task._label));
this.showOutput();
}
const toDispose = new DisposableStore();
let eventCounter: number = 0;
const mapKey = task.getMapKey();
toDispose.add(watchingProblemMatcher.onDidStateChange((event) => {
if (event.kind === ProblemCollectorEventKind.BackgroundProcessingBegins) {
eventCounter++;
this.busyTasks[mapKey] = task;
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Active, task));
} else if (event.kind === ProblemCollectorEventKind.BackgroundProcessingEnds) {
eventCounter--;
if (this.busyTasks[mapKey]) {
delete this.busyTasks[mapKey];
}
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);
}
}
}
}
}));
watchingProblemMatcher.aboutToStart();
let delayer: Async.Delayer<any> | undefined = undefined;
[terminal, executedCommand, error] = await this.createTerminal(task, resolver, workspaceFolder);
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) {
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!));
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);
let skipLine: boolean = (!!task.command.presentation && task.command.presentation.echo);
const onData = terminal.onLineData((line) => {
if (skipLine) {
skipLine = false;
return;
}
watchingProblemMatcher.processLine(line);
if (!delayer) {
delayer = new Async.Delayer(3000);
}
delayer.trigger(() => {
watchingProblemMatcher.forceDelivery();
delayer = undefined;
});
});
promise = new Promise<ITaskSummary>((resolve, reject) => {
const onExit = terminal!.onExit((exitCode) => {
onData.dispose();
onExit.dispose();
let key = task.getMapKey();
if (this.busyTasks[mapKey]) {
delete this.busyTasks[mapKey];
}
delete this.activeTasks[key];
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Changed));
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;
}
}
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!);
this.terminalService.showPanel(false);
}
watchingProblemMatcher.done();
watchingProblemMatcher.dispose();
registeredLinkMatchers.forEach(handle => terminal!.deregisterLinkMatcher(handle));
if (!processStartedSignaled) {
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!));
processStartedSignaled = true;
}
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode));
for (let i = 0; i < eventCounter; i++) {
let event = TaskEvent.create(TaskEventKind.Inactive, task);
this._onDidStateChange.fire(event);
}
eventCounter = 0;
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task));
toDispose.dispose();
resolve({ exitCode });
});
});
} else {
[terminal, executedCommand, error] = await this.createTerminal(task, resolver, workspaceFolder);
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) {
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!));
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 mapKey = task.getMapKey();
this.busyTasks[mapKey] = task;
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);
let skipLine: boolean = (!!task.command.presentation && task.command.presentation.echo);
const onData = terminal.onLineData((line) => {
if (skipLine) {
skipLine = false;
return;
}
startStopProblemMatcher.processLine(line);
});
promise = new Promise<ITaskSummary>((resolve, reject) => {
const onExit = terminal!.onExit((exitCode) => {
onData.dispose();
onExit.dispose();
let key = task.getMapKey();
delete this.activeTasks[key];
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Changed));
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;
}
}
let reveal = task.command.presentation!.reveal;
let revealProblems = task.command.presentation!.revealProblems;
let revealProblemPanel = terminal && (revealProblems === RevealProblemKind.OnProblem) && (startStopProblemMatcher.numberOfMatches > 0);
if (revealProblemPanel) {
this.panelService.openPanel(Constants.MARKERS_PANEL_ID);
} else if (terminal && (reveal === RevealKind.Silent) && ((exitCode !== 0) || (startStopProblemMatcher.numberOfMatches > 0) && startStopProblemMatcher.maxMarkerSeverity &&
(startStopProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error))) {
this.terminalService.setActiveInstance(terminal);
this.terminalService.showPanel(false);
}
startStopProblemMatcher.done();
startStopProblemMatcher.dispose();
registeredLinkMatchers.forEach(handle => {
if (terminal) {
terminal.deregisterLinkMatcher(handle);
}
});
if (!processStartedSignaled && terminal) {
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal.processId!));
processStartedSignaled = true;
}
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode));
if (this.busyTasks[mapKey]) {
delete this.busyTasks[mapKey];
}
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Inactive, task));
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.End, task));
resolve({ exitCode });
});
});
}
let showProblemPanel = task.command.presentation && (task.command.presentation.revealProblems === RevealProblemKind.Always);
if (showProblemPanel) {
this.panelService.openPanel(Constants.MARKERS_PANEL_ID);
} else if (task.command.presentation && (task.command.presentation.reveal === RevealKind.Always)) {
this.terminalService.setActiveInstance(terminal);
this.terminalService.showPanel(task.command.presentation.focus);
}
this.activeTasks[task.getMapKey()] = { terminal, task, promise };
this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Changed));
return promise.then((summary) => {
try {
let telemetryEvent: TelemetryEvent = {
trigger: trigger,
runner: 'terminal',
taskKind: task.getTelemetryKind(),
command: this.getSanitizedCommand(executedCommand!),
success: true,
exitCode: summary.exitCode
};
/* __GDPR__
"taskService" : {
"${include}": [
"${TelemetryEvent}"
]
}
*/
this.telemetryService.publicLog(TerminalTaskSystem.TelemetryEventName, telemetryEvent);
} catch (error) {
}
return summary;
}, (error) => {
try {
let telemetryEvent: TelemetryEvent = {
trigger: trigger,
runner: 'terminal',
taskKind: task.getTelemetryKind(),
command: this.getSanitizedCommand(executedCommand!),
success: false
};
/* __GDPR__
"taskService" : {
"${include}": [
"${TelemetryEvent}"
]
}
*/
this.telemetryService.publicLog(TerminalTaskSystem.TelemetryEventName, telemetryEvent);
} catch (error) {
}
return Promise.reject<ITaskSummary>(error);
});
}
private createTerminalName(task: CustomTask | ContributedTask): string {
const needsFolderQualification = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE;
return nls.localize('TerminalTaskSystem.terminalName', 'Task - {0}', needsFolderQualification ? task.getQualifiedLabel() : task.configurationProperties.name);
}
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> {
let shellLaunchConfig: IShellLaunchConfig;
let isShellCommand = task.command.runtime === RuntimeType.Shell;
let needsFolderQualification = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE;
let terminalName = this.createTerminalName(task);
let originalCommand = task.command.name;
if (isShellCommand) {
const defaultConfig = variableResolver.taskSystemInfo ? await variableResolver.taskSystemInfo.getDefaultShellAndArgs() : await this.terminalInstanceService.getDefaultShellAndArgs(true, platform);
shellLaunchConfig = { name: terminalName, executable: defaultConfig.shell, args: defaultConfig.args, waitOnExit };
let shellSpecified: boolean = false;
let shellOptions: ShellConfiguration | undefined = task.command.options && task.command.options.shell;
if (shellOptions) {
if (shellOptions.executable) {
shellLaunchConfig.executable = this.resolveVariable(variableResolver, shellOptions.executable);
shellSpecified = true;
}
if (shellOptions.args) {
shellLaunchConfig.args = this.resolveVariables(variableResolver, shellOptions.args.slice());
} else {
shellLaunchConfig.args = [];
}
}
let shellArgs = Array.isArray(shellLaunchConfig.args!) ? <string[]>shellLaunchConfig.args!.slice(0) : [shellLaunchConfig.args!];
let toAdd: string[] = [];
let commandLine = this.buildShellCommandLine(platform, shellLaunchConfig.executable!, shellOptions, command, originalCommand, args);
let windowsShellArgs: boolean = false;
if (platform === Platform.Platform.Windows) {
windowsShellArgs = true;
let basename = path.basename(shellLaunchConfig.executable!).toLowerCase();
// If we don't have a cwd, then the terminal uses the home dir.
const userHome = await this.remotePathService.userHome;
if (basename === 'cmd.exe' && ((options.cwd && isUNC(options.cwd)) || (!options.cwd && isUNC(userHome.fsPath)))) {
return undefined;
}
if ((basename === 'powershell.exe') || (basename === 'pwsh.exe')) {
if (!shellSpecified) {
toAdd.push('-Command');
}
} else if ((basename === 'bash.exe') || (basename === 'zsh.exe')) {
windowsShellArgs = false;
if (!shellSpecified) {
toAdd.push('-c');
}
} else if (basename === 'wsl.exe') {
if (!shellSpecified) {
toAdd.push('-e');
}
} else {
if (!shellSpecified) {
toAdd.push('/d', '/c');
}
}
} else {
if (!shellSpecified) {
// Under Mac remove -l to not start it as a login shell.
if (platform === Platform.Platform.Mac) {
let index = shellArgs.indexOf('-l');
if (index !== -1) {
shellArgs.splice(index, 1);
}
}
toAdd.push('-c');
}
}
toAdd.forEach(element => {
if (!shellArgs.some(arg => arg.toLowerCase() === element)) {
shellArgs.push(element);
}
});
shellArgs.push(commandLine);
shellLaunchConfig.args = windowsShellArgs ? shellArgs.join(' ') : shellArgs;
if (task.command.presentation && task.command.presentation.echo) {
if (needsFolderQualification && workspaceFolder) {
shellLaunchConfig.initialText = `\x1b[1m> Executing task in folder ${workspaceFolder.name}: ${commandLine} <\x1b[0m\n`;
} else {
shellLaunchConfig.initialText = `\x1b[1m> Executing task: ${commandLine} <\x1b[0m\n`;
}
}
} else {
let commandExecutable = (task.command.runtime !== RuntimeType.CustomExecution) ? CommandString.value(command) : undefined;
let executable = !isShellCommand
? this.resolveVariable(variableResolver, '${' + TerminalTaskSystem.ProcessVarName + '}')
: commandExecutable;
// When we have a process task there is no need to quote arguments. So we go ahead and take the string value.
shellLaunchConfig = {
name: terminalName,
executable: executable,
args: args.map(a => Types.isString(a) ? a : a.value),
waitOnExit
};
if (task.command.presentation && task.command.presentation.echo) {
let getArgsToEcho = (args: string | string[] | undefined): string => {
if (!args || args.length === 0) {
return '';
}
if (Types.isString(args)) {
return args;
}
return args.join(' ');
};
if (needsFolderQualification && workspaceFolder) {
shellLaunchConfig.initialText = `\x1b[1m> Executing task in folder ${workspaceFolder.name}: ${shellLaunchConfig.executable} ${getArgsToEcho(shellLaunchConfig.args)} <\x1b[0m\n`;
} else {
shellLaunchConfig.initialText = `\x1b[1m> Executing task: ${shellLaunchConfig.executable} ${getArgsToEcho(shellLaunchConfig.args)} <\x1b[0m\n`;
}
}
}
if (options.cwd) {
let cwd = options.cwd;
if (!path.isAbsolute(cwd)) {
if (workspaceFolder && (workspaceFolder.uri.scheme === 'file')) {
cwd = path.join(workspaceFolder.uri.fsPath, cwd);
}
}
// This must be normalized to the OS
shellLaunchConfig.cwd = resources.toLocalResource(URI.from({ scheme: Schemas.file, path: cwd }), this.environmentService.configuration.remoteAuthority);
}
if (options.env) {
shellLaunchConfig.env = options.env;
}
return shellLaunchConfig;
}
private async createTerminal(task: CustomTask | ContributedTask, resolver: VariableResolver, workspaceFolder: IWorkspaceFolder | undefined): Promise<[ITerminalInstance | undefined, string | undefined, TaskError | undefined]> {
let platform = resolver.taskSystemInfo ? resolver.taskSystemInfo.platform : Platform.platform;
let options = this.resolveOptions(resolver, task.command.options);
let waitOnExit: boolean | string = false;
const presentationOptions = task.command.presentation;
if (!presentationOptions) {
throw new Error('Task presentation options should not be undefined here.');
}
if (presentationOptions.reveal !== RevealKind.Never || !task.configurationProperties.isBackground) {
if (presentationOptions.panel === PanelKind.New) {
waitOnExit = nls.localize('closeTerminal', 'Press any key to close the terminal.');
} else if (presentationOptions.showReuseMessage) {
waitOnExit = nls.localize('reuseTerminal', 'Terminal will be reused by tasks, press any key to close it.');
} else {
waitOnExit = true;
}
}
let commandExecutable: string | undefined;
let command: CommandString | undefined;
let args: CommandString[] | undefined;
let launchConfigs: IShellLaunchConfig | undefined;
if (task.command.runtime === RuntimeType.CustomExecution) {
this.currentTask.shellLaunchConfig = launchConfigs = {
isExtensionTerminal: true,
waitOnExit,
name: this.createTerminalName(task),
initialText: task.command.presentation && task.command.presentation.echo ? `\x1b[1m> Executing task: ${task._label} <\x1b[0m\n` : undefined
};
} else {
let resolvedResult: { command: CommandString, args: CommandString[] } = this.resolveCommandAndArgs(resolver, task.command);
command = resolvedResult.command;
args = resolvedResult.args;
commandExecutable = CommandString.value(command);
this.currentTask.shellLaunchConfig = launchConfigs = (this.isRerun && this.lastTask) ? this.lastTask.getVerifiedTask().shellLaunchConfig : await this.createShellLaunchConfig(task, workspaceFolder, resolver, platform, options, command, args, waitOnExit);
if (launchConfigs === undefined) {
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)];
}
}
let prefersSameTerminal = presentationOptions.panel === PanelKind.Dedicated;
let allowsSharedTerminal = presentationOptions.panel === PanelKind.Shared;
let group = presentationOptions.group;
let taskKey = task.getMapKey();
let terminalToReuse: TerminalData | undefined;
if (prefersSameTerminal) {
let terminalId = this.sameTaskTerminals[taskKey];
if (terminalId) {
terminalToReuse = this.terminals[terminalId];
delete this.sameTaskTerminals[taskKey];
}
} else if (allowsSharedTerminal) {
// 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)!;
if (idleTerminalId && this.terminals[idleTerminalId] && this.terminals[idleTerminalId].group === group) {
terminalId = this.idleTaskTerminals.remove(taskId);
break;
}
}
}
if (terminalId) {
terminalToReuse = this.terminals[terminalId];
}
}
if (terminalToReuse) {
if (!launchConfigs) {
throw new Error('Task shell launch configuration should not be undefined here.');
}
terminalToReuse.terminal.scrollToBottom();
terminalToReuse.terminal.reuseTerminal(launchConfigs);
if (task.command.presentation && task.command.presentation.clear) {
terminalToReuse.terminal.clear();
}
this.terminals[terminalToReuse.terminal.id.toString()].lastTask = taskKey;
return [terminalToReuse.terminal, commandExecutable, undefined];
}
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;
await originalInstance.waitForTitle();
result = this.terminalService.splitInstance(originalInstance, launchConfigs);
if (result) {
break;
}
}
}
}
if (!result) {
// Either no group is used, no terminal with the group exists or splitting an existing terminal failed.
result = this.terminalService.createTerminal(launchConfigs);
}
const terminalKey = result.id.toString();
result.onDisposed((terminal) => {
let terminalData = this.terminals[terminalKey];
if (terminalData) {
delete this.terminals[terminalKey];
delete this.sameTaskTerminals[terminalData.lastTask];
this.idleTaskTerminals.delete(terminalData.lastTask);
// 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.
const mapKey = task.getMapKey();
delete this.activeTasks[mapKey];
if (this.busyTasks[mapKey]) {
delete this.busyTasks[mapKey];
}
}
});
this.terminals[terminalKey] = { terminal: result, lastTask: taskKey, group };
return [result, commandExecutable, undefined];
}
private buildShellCommandLine(platform: Platform.Platform, shellExecutable: string, shellOptions: ShellConfiguration | undefined, command: CommandString, originalCommand: CommandString | undefined, args: CommandString[]): string {
let basename = path.parse(shellExecutable).name.toLowerCase();
let shellQuoteOptions = this.getQuotingOptions(basename, shellOptions, platform);
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;
}
}
let quote: string | undefined;
for (let i = 0; i < value.length; i++) {
// We found the end quote.
let ch = value[i];
if (ch === quote) {
quote = undefined;
} else if (quote !== undefined) {
// 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 === ' ') {
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);
}
}
// 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;
}
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
if (platform === Platform.Platform.Windows) {
if (basename === 'cmd' && commandQuoted && argQuoted) {
commandLine = '"' + commandLine + '"';
} else if (basename === 'powershell' && commandQuoted) {
commandLine = '& ' + commandLine;
}
}
if (basename === 'cmd' && platform === Platform.Platform.Windows && commandQuoted && argQuoted) {
commandLine = '"' + commandLine + '"';
}
return commandLine;
}
private getQuotingOptions(shellBasename: string, shellOptions: ShellConfiguration | undefined, platform: Platform.Platform): ShellQuotingOptions {
if (shellOptions && shellOptions.quoting) {
return shellOptions.quoting;
}
return TerminalTaskSystem.shellQuotes[shellBasename] || TerminalTaskSystem.osShellQuotes[Platform.PlatformToString(platform)];
}
private collectTaskVariables(variables: Set<string>, task: CustomTask | ContributedTask): void {
if (task.command && task.command.name) {
this.collectCommandVariables(variables, task.command, task);
}
this.collectMatcherVariables(variables, task.configurationProperties.problemMatchers);
}
private collectCommandVariables(variables: Set<string>, command: CommandConfiguration, task: CustomTask | ContributedTask): void {
// The custom execution should have everything it needs already as it provided
// the callback.
if (command.runtime === RuntimeType.CustomExecution) {
return;
}
if (command.name === undefined) {
throw new Error('Command name should never be undefined here.');
}
this.collectVariables(variables, command.name);
if (command.args) {
command.args.forEach(arg => this.collectVariables(variables, arg));
}
// Try to get a scope.
const scope = (<ExtensionTaskSource>task._source).scope;
if (scope !== TaskScope.Global) {
variables.add('${workspaceFolder}');
}
if (command.options) {
let options = command.options;
if (options.cwd) {
this.collectVariables(variables, options.cwd);
}
const optionsEnv = options.env;
if (optionsEnv) {
Object.keys(optionsEnv).forEach((key) => {
let value: any = optionsEnv[key];
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));
}
}
}
}
private collectMatcherVariables(variables: Set<string>, values: Array<string | ProblemMatcher> | undefined): void {
if (values === undefined || values === null || values.length === 0) {
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;
let matches: RegExpExecArray | null;
do {
matches = r.exec(string);
if (matches) {
variables.add(matches[0]);
}
} while (matches);
}
private resolveCommandAndArgs(resolver: VariableResolver, commandConfig: CommandConfiguration): { command: CommandString, args: CommandString[] } {
// First we need to use the command args:
let args: CommandString[] = commandConfig.args ? commandConfig.args.slice() : [];
args = this.resolveVariables(resolver, args);
let command: CommandString = this.resolveVariable(resolver, commandConfig.name);
return { command, args };
}
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));
}
private resolveMatchers(resolver: VariableResolver, values: Array<string | ProblemMatcher> | undefined): ProblemMatcher[] {
if (values === undefined || values === null || values.length === 0) {
return [];
}
let result: ProblemMatcher[] = [];
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) {
this.appendOutput(nls.localize('unknownProblemMatcher', 'Problem matcher {0} can\'t be resolved. The matcher will be ignored'));
return;
}
let taskSystemInfo: TaskSystemInfo | undefined = resolver.taskSystemInfo;
let hasFilePrefix = matcher.filePrefix !== undefined;
let hasUriProvider = taskSystemInfo !== undefined && taskSystemInfo.uriProvider !== undefined;
if (!hasFilePrefix && !hasUriProvider) {
result.push(matcher);
} else {
let copy = Objects.deepClone(matcher);
if (hasUriProvider && (taskSystemInfo !== undefined)) {
copy.uriProvider = taskSystemInfo.uriProvider;
}
if (hasFilePrefix) {
copy.filePrefix = this.resolveVariable(resolver, copy.filePrefix);
}
result.push(copy);
}
});
return result;
}
private resolveVariable(resolver: VariableResolver, value: string | undefined): string;
private resolveVariable(resolver: VariableResolver, value: CommandString | undefined): CommandString;
private resolveVariable(resolver: VariableResolver, value: CommandString | undefined): CommandString {
// TODO@Dirk Task.getWorkspaceFolder should return a WorkspaceFolder that is defined in workspace.ts
if (Types.isString(value)) {
return resolver.resolve(value);
} else if (value !== undefined) {
return {
value: resolver.resolve(value.value),
quoting: value.quoting
};
} else { // This should never happen
throw new Error('Should never try to resolve undefined.');
}
}
private resolveOptions(resolver: VariableResolver, options: CommandOptions | undefined): CommandOptions {
if (options === undefined || options === null) {
let cwd: string | undefined;
try {
cwd = this.resolveVariable(resolver, '${workspaceFolder}');
} catch (e) {
// No workspace
}
return { cwd };
}
let result: CommandOptions = Types.isString(options.cwd)
? { cwd: this.resolveVariable(resolver, options.cwd) }
: { cwd: this.resolveVariable(resolver, '${workspaceFolder}') };
if (options.env) {
result.env = Object.create(null);
Object.keys(options.env).forEach((key) => {
let value: any = options.env![key];
if (Types.isString(value)) {
result.env![key] = this.resolveVariable(resolver, value);
} else {
result.env![key] = value.toString();
}
});
}
return result;
}
private registerLinkMatchers(terminal: ITerminalInstance, problemMatchers: ProblemMatcher[]): number[] {
let result: number[] = [];
/*
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
});
}
}, 0));
}
};
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);
}
}
*/
return result;
}
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
};
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';
}
private appendOutput(output: string): void {
const outputChannel = this.outputService.getChannel(this.outputChannelId);
if (outputChannel) {
outputChannel.append(output);
}
}
private async fileExists(path: string): Promise<boolean> {
const uri: URI = resources.toLocalResource(URI.from({ scheme: Schemas.file, path: path }), this.environmentService.configuration.remoteAuthority);
if (await this.fileService.exists(uri)) {
return !((await this.fileService.resolve(uri)).isDirectory);
}
return false;
}
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) {
cwd = processCwd();
}
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);
}
if (paths === undefined && Types.isString(processEnv.PATH)) {
paths = processEnv.PATH.split(path.delimiter);
}
// 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.fileExists(fullPath)) {
return fullPath;
}
let withExtension = fullPath + '.com';
if (await this.fileExists(withExtension)) {
return withExtension;
}
withExtension = fullPath + '.exe';
if (await this.fileExists(withExtension)) {
return withExtension;
}
}
return path.join(cwd, command);
}
}