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

6
import * as Collections from 'vs/base/common/collections';
7
import * as Objects from 'vs/base/common/objects';
8
import * as Path from 'vs/base/common/path';
9
import { CommandOptions, ErrorData, Source } from 'vs/base/common/processes';
10
import * as Strings from 'vs/base/common/strings';
11 12 13 14 15 16
import { LineData, LineProcess } from 'vs/base/node/processes';
import * as nls from 'vs/nls';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import * as Tasks from '../common/tasks';
17
import * as TaskConfig from '../common/taskConfiguration';
E
Erich Gamma 已提交
18

M
Matt Bierner 已提交
19 20 21
const build = 'build';
const test = 'test';
const defaultValue = 'default';
E
Erich Gamma 已提交
22 23 24 25 26 27 28 29 30 31 32 33

interface TaskInfo {
	index: number;
	exact: number;
}

interface TaskInfos {
	build: TaskInfo;
	test: TaskInfo;
}

interface TaskDetectorMatcher {
34
	init(): void;
35
	match(tasks: string[], line: string): void;
E
Erich Gamma 已提交
36 37 38 39 40 41 42 43 44 45
}

interface DetectorConfig {
	matcher: TaskDetectorMatcher;
	arg: string;
}

class RegexpTaskMatcher implements TaskDetectorMatcher {
	private regexp: RegExp;

J
Johannes Rieken 已提交
46
	constructor(regExp: RegExp) {
E
Erich Gamma 已提交
47 48 49 50 51 52
		this.regexp = regExp;
	}

	init() {
	}

53
	match(tasks: string[], line: string): void {
E
Erich Gamma 已提交
54 55 56 57 58 59 60 61
		let matches = this.regexp.exec(line);
		if (matches && matches.length > 0) {
			tasks.push(matches[1]);
		}
	}
}

class GruntTaskMatcher implements TaskDetectorMatcher {
62 63 64
	private tasksStart!: boolean;
	private tasksEnd!: boolean;
	private descriptionOffset!: number | null;
E
Erich Gamma 已提交
65 66 67 68

	init() {
		this.tasksStart = false;
		this.tasksEnd = false;
69
		this.descriptionOffset = null;
E
Erich Gamma 已提交
70 71
	}

72
	match(tasks: string[], line: string): void {
J
Johannes Rieken 已提交
73 74 75 76 77 78 79 80 81 82 83 84 85 86
		// grunt lists tasks as follows (description is wrapped into a new line if too long):
		// ...
		// Available tasks
		//         uglify  Minify files with UglifyJS. *
		//         jshint  Validate files with JSHint. *
		//           test  Alias for "jshint", "qunit" tasks.
		//        default  Alias for "jshint", "qunit", "concat", "uglify" tasks.
		//           long  Alias for "eslint", "qunit", "browserify", "sass",
		//                 "autoprefixer", "uglify", tasks.
		//
		// Tasks run in the order specified
		if (!this.tasksStart && !this.tasksEnd) {
			if (line.indexOf('Available tasks') === 0) {
				this.tasksStart = true;
E
Erich Gamma 已提交
87
			}
J
Johannes Rieken 已提交
88 89 90 91 92 93
		}
		else if (this.tasksStart && !this.tasksEnd) {
			if (line.indexOf('Tasks run in the order specified') === 0) {
				this.tasksEnd = true;
			} else {
				if (this.descriptionOffset === null) {
94 95 96 97 98 99
					const match = line.match(/\S  \S/);
					if (match) {
						this.descriptionOffset = (match.index || 0) + 1;
					} else {
						this.descriptionOffset = 0;
					}
J
Johannes Rieken 已提交
100 101 102 103
				}
				let taskName = line.substr(0, this.descriptionOffset).trim();
				if (taskName.length > 0) {
					tasks.push(taskName);
E
Erich Gamma 已提交
104 105
				}
			}
J
Johannes Rieken 已提交
106
		}
E
Erich Gamma 已提交
107 108 109
	}
}

110
export interface DetectorResult {
111
	config: TaskConfig.ExternalTaskRunnerConfiguration | null;
112 113 114 115
	stdout: string[];
	stderr: string[];
}

E
Erich Gamma 已提交
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
export class ProcessRunnerDetector {

	private static Version: string = '0.1.0';

	private static SupportedRunners: Collections.IStringDictionary<boolean> = {
		'gulp': true,
		'jake': true,
		'grunt': true
	};

	private static TaskMatchers: Collections.IStringDictionary<DetectorConfig> = {
		'gulp': { matcher: new RegexpTaskMatcher(/^(.*)$/), arg: '--tasks-simple' },
		'jake': { matcher: new RegexpTaskMatcher(/^jake\s+([^\s]+)\s/), arg: '--tasks' },
		'grunt': { matcher: new GruntTaskMatcher(), arg: '--help' },
	};

	public static supports(runner: string): boolean {
		return ProcessRunnerDetector.SupportedRunners[runner];
	}

J
Johannes Rieken 已提交
136
	private static detectorConfig(runner: string): DetectorConfig {
E
Erich Gamma 已提交
137 138 139 140 141 142 143
		return ProcessRunnerDetector.TaskMatchers[runner];
	}

	private static DefaultProblemMatchers: string[] = ['$lessCompile', '$tsc', '$jshint'];

	private fileService: IFileService;
	private contextService: IWorkspaceContextService;
144
	private configurationResolverService: IConfigurationResolverService;
145
	private taskConfiguration: TaskConfig.ExternalTaskRunnerConfiguration | null;
D
Dirk Baeumer 已提交
146
	private _workspaceRoot: IWorkspaceFolder;
E
Erich Gamma 已提交
147
	private _stderr: string[];
148
	private _stdout: string[];
149
	private _cwd: string;
E
Erich Gamma 已提交
150

151
	constructor(workspaceFolder: IWorkspaceFolder, fileService: IFileService, contextService: IWorkspaceContextService, configurationResolverService: IConfigurationResolverService, config: TaskConfig.ExternalTaskRunnerConfiguration | null = null) {
E
Erich Gamma 已提交
152 153
		this.fileService = fileService;
		this.contextService = contextService;
154
		this.configurationResolverService = configurationResolverService;
E
Erich Gamma 已提交
155
		this.taskConfiguration = config;
D
Dirk Baeumer 已提交
156
		this._workspaceRoot = workspaceFolder;
E
Erich Gamma 已提交
157
		this._stderr = [];
158
		this._stdout = [];
159
		this._cwd = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? Path.normalize(this._workspaceRoot.uri.fsPath) : '';
E
Erich Gamma 已提交
160 161 162 163 164 165
	}

	public get stderr(): string[] {
		return this._stderr;
	}

166 167 168 169
	public get stdout(): string[] {
		return this._stdout;
	}

170
	public detect(list: boolean = false, detectSpecific?: string): Promise<DetectorResult> {
171 172
		let commandExecutable: string;
		if (this.taskConfiguration && this.taskConfiguration.command && (commandExecutable = TaskConfig.CommandString.value(this.taskConfiguration.command)) && ProcessRunnerDetector.supports(commandExecutable)) {
173
			let config = ProcessRunnerDetector.detectorConfig(commandExecutable);
E
Erich Gamma 已提交
174
			let args = (this.taskConfiguration.args || []).concat(config.arg);
D
Dirk Baeumer 已提交
175
			let options: CommandOptions = this.taskConfiguration.options ? this.resolveCommandOptions(this._workspaceRoot, this.taskConfiguration.options) : { cwd: this._cwd };
E
Erich Gamma 已提交
176
			let isShellCommand = !!this.taskConfiguration.isShellCommand;
177
			return Promise.resolve(this.runDetection(
178
				new LineProcess(commandExecutable, this.configurationResolverService.resolve(this._workspaceRoot, args.map(a => TaskConfig.CommandString.value(a))), isShellCommand, options),
179
				commandExecutable, isShellCommand, config.matcher, ProcessRunnerDetector.DefaultProblemMatchers, list));
E
Erich Gamma 已提交
180
		} else {
181
			if (detectSpecific) {
182
				let detectorPromise: Promise<DetectorResult | null>;
183
				if ('gulp' === detectSpecific) {
D
Dirk Baeumer 已提交
184
					detectorPromise = this.tryDetectGulp(this._workspaceRoot, list);
185
				} else if ('jake' === detectSpecific) {
D
Dirk Baeumer 已提交
186
					detectorPromise = this.tryDetectJake(this._workspaceRoot, list);
187
				} else if ('grunt' === detectSpecific) {
D
Dirk Baeumer 已提交
188
					detectorPromise = this.tryDetectGrunt(this._workspaceRoot, list);
189
				} else {
J
Julien Brianceau 已提交
190
					throw new Error('Unknown detector type');
E
Erich Gamma 已提交
191
				}
192
				return detectorPromise.then((value) => {
E
Erich Gamma 已提交
193 194
					if (value) {
						return value;
195
					} else {
196
						return { config: null, stdout: this.stdout, stderr: this.stderr };
E
Erich Gamma 已提交
197
					}
198 199
				});
			} else {
D
Dirk Baeumer 已提交
200
				return this.tryDetectGulp(this._workspaceRoot, list).then((value) => {
201 202 203
					if (value) {
						return value;
					}
D
Dirk Baeumer 已提交
204
					return this.tryDetectJake(this._workspaceRoot, list).then((value) => {
E
Erich Gamma 已提交
205 206 207
						if (value) {
							return value;
						}
D
Dirk Baeumer 已提交
208
						return this.tryDetectGrunt(this._workspaceRoot, list).then((value) => {
209 210 211
							if (value) {
								return value;
							}
212
							return { config: null, stdout: this.stdout, stderr: this.stderr };
213
						});
A
tslint  
Alex Dima 已提交
214
					});
E
Erich Gamma 已提交
215
				});
216
			}
E
Erich Gamma 已提交
217 218 219
		}
	}

D
Dirk Baeumer 已提交
220
	private resolveCommandOptions(workspaceFolder: IWorkspaceFolder, options: CommandOptions): CommandOptions {
C
ChaseKnowlden 已提交
221
		// TODO@Dirk adopt new configuration resolver service https://github.com/microsoft/vscode/issues/31365
J
Johannes Rieken 已提交
222
		let result = Objects.deepClone(options);
223
		if (result.cwd) {
D
Dirk Baeumer 已提交
224
			result.cwd = this.configurationResolverService.resolve(workspaceFolder, result.cwd);
225 226
		}
		if (result.env) {
D
Dirk Baeumer 已提交
227
			result.env = this.configurationResolverService.resolve(workspaceFolder, result.env);
228 229 230 231
		}
		return result;
	}

232
	private tryDetectGulp(workspaceFolder: IWorkspaceFolder, list: boolean): Promise<DetectorResult | null> {
C
ChaseKnowlden 已提交
233
		return Promise.resolve(this.fileService.resolve(workspaceFolder.toResource('gulpfile.js'))).then((stat) => { // TODO@Dirk (https://github.com/microsoft/vscode/issues/29454)
E
Erich Gamma 已提交
234
			let config = ProcessRunnerDetector.detectorConfig('gulp');
J
Johannes Rieken 已提交
235
			let process = new LineProcess('gulp', [config.arg, '--no-color'], true, { cwd: this._cwd });
E
Erich Gamma 已提交
236
			return this.runDetection(process, 'gulp', true, config.matcher, ProcessRunnerDetector.DefaultProblemMatchers, list);
R
Ron Buckton 已提交
237
		}, (err: any) => {
E
Erich Gamma 已提交
238 239 240 241
			return null;
		});
	}

242
	private tryDetectGrunt(workspaceFolder: IWorkspaceFolder, list: boolean): Promise<DetectorResult | null> {
C
ChaseKnowlden 已提交
243
		return Promise.resolve(this.fileService.resolve(workspaceFolder.toResource('Gruntfile.js'))).then((stat) => { // TODO@Dirk (https://github.com/microsoft/vscode/issues/29454)
E
Erich Gamma 已提交
244
			let config = ProcessRunnerDetector.detectorConfig('grunt');
J
Johannes Rieken 已提交
245
			let process = new LineProcess('grunt', [config.arg, '--no-color'], true, { cwd: this._cwd });
E
Erich Gamma 已提交
246
			return this.runDetection(process, 'grunt', true, config.matcher, ProcessRunnerDetector.DefaultProblemMatchers, list);
R
Ron Buckton 已提交
247
		}, (err: any) => {
E
Erich Gamma 已提交
248 249 250 251
			return null;
		});
	}

252
	private tryDetectJake(workspaceFolder: IWorkspaceFolder, list: boolean): Promise<DetectorResult | null> {
D
Dirk Baeumer 已提交
253
		let run = () => {
E
Erich Gamma 已提交
254
			let config = ProcessRunnerDetector.detectorConfig('jake');
J
Johannes Rieken 已提交
255
			let process = new LineProcess('jake', [config.arg], true, { cwd: this._cwd });
E
Erich Gamma 已提交
256
			return this.runDetection(process, 'jake', true, config.matcher, ProcessRunnerDetector.DefaultProblemMatchers, list);
D
Dirk Baeumer 已提交
257
		};
C
ChaseKnowlden 已提交
258
		return Promise.resolve(this.fileService.resolve(workspaceFolder.toResource('Jakefile'))).then((stat) => { // TODO@Dirk (https://github.com/microsoft/vscode/issues/29454)
D
Dirk Baeumer 已提交
259 260
			return run();
		}, (err: any) => {
C
ChaseKnowlden 已提交
261
			return this.fileService.resolve(workspaceFolder.toResource('Jakefile.js')).then((stat) => { // TODO@Dirk (https://github.com/microsoft/vscode/issues/29454)
D
Dirk Baeumer 已提交
262
				return run();
R
Ron Buckton 已提交
263
			}, (err: any) => {
D
Dirk Baeumer 已提交
264 265
				return null;
			});
E
Erich Gamma 已提交
266 267 268
		});
	}

J
Johannes Rieken 已提交
269
	private runDetection(process: LineProcess, command: string, isShellCommand: boolean, matcher: TaskDetectorMatcher, problemMatchers: string[], list: boolean): Promise<DetectorResult> {
J
Johannes Rieken 已提交
270
		let tasks: string[] = [];
E
Erich Gamma 已提交
271
		matcher.init();
J
Joao Moreno 已提交
272 273 274 275 276 277 278

		const onProgress = (progress: LineData) => {
			if (progress.source === Source.stderr) {
				this._stderr.push(progress.line);
				return;
			}
			let line = Strings.removeAnsiEscapeCodes(progress.line);
279
			matcher.match(tasks, line);
J
Joao Moreno 已提交
280 281 282
		};

		return process.start(onProgress).then((success) => {
E
Erich Gamma 已提交
283 284 285 286 287 288 289 290
			if (tasks.length === 0) {
				if (success.cmdCode !== 0) {
					if (command === 'gulp') {
						this._stderr.push(nls.localize('TaskSystemDetector.noGulpTasks', 'Running gulp --tasks-simple didn\'t list any tasks. Did you run npm install?'));
					} else if (command === 'jake') {
						this._stderr.push(nls.localize('TaskSystemDetector.noJakeTasks', 'Running jake --tasks didn\'t list any tasks. Did you run npm install?'));
					}
				}
291
				return { config: null, stdout: this._stdout, stderr: this._stderr };
E
Erich Gamma 已提交
292
			}
293
			let result: TaskConfig.ExternalTaskRunnerConfiguration = {
E
Erich Gamma 已提交
294 295 296 297 298 299 300 301 302
				version: ProcessRunnerDetector.Version,
				command: command,
				isShellCommand: isShellCommand
			};
			// Hack. We need to remove this.
			if (command === 'gulp') {
				result.args = ['--no-color'];
			}
			result.tasks = this.createTaskDescriptions(tasks, problemMatchers, list);
303
			return { config: result, stdout: this._stdout, stderr: this._stderr };
E
Erich Gamma 已提交
304 305 306 307 308 309 310 311
		}, (err: ErrorData) => {
			let error = err.error;
			if ((<any>error).code === 'ENOENT') {
				if (command === 'gulp') {
					this._stderr.push(nls.localize('TaskSystemDetector.noGulpProgram', 'Gulp is not installed on your system. Run npm install -g gulp to install it.'));
				} else if (command === 'jake') {
					this._stderr.push(nls.localize('TaskSystemDetector.noJakeProgram', 'Jake is not installed on your system. Run npm install -g jake to install it.'));
				} else if (command === 'grunt') {
312
					this._stderr.push(nls.localize('TaskSystemDetector.noGruntProgram', 'Grunt is not installed on your system. Run npm install -g grunt to install it.'));
E
Erich Gamma 已提交
313 314
				}
			} else {
315
				this._stderr.push(nls.localize('TaskSystemDetector.noProgram', 'Program {0} was not found. Message is {1}', command, error ? error.message : ''));
E
Erich Gamma 已提交
316
			}
317
			return { config: null, stdout: this._stdout, stderr: this._stderr };
E
Erich Gamma 已提交
318 319 320
		});
	}

321 322
	private createTaskDescriptions(tasks: string[], problemMatchers: string[], list: boolean): TaskConfig.CustomTask[] {
		let taskConfigs: TaskConfig.CustomTask[] = [];
E
Erich Gamma 已提交
323 324 325 326
		if (list) {
			tasks.forEach((task) => {
				taskConfigs.push({
					taskName: task,
327
					args: []
E
Erich Gamma 已提交
328 329 330 331 332 333 334 335 336 337 338 339
				});
			});
		} else {
			let taskInfos: TaskInfos = {
				build: { index: -1, exact: -1 },
				test: { index: -1, exact: -1 }
			};
			tasks.forEach((task, index) => {
				this.testBuild(taskInfos.build, task, index);
				this.testTest(taskInfos.test, task, index);
			});
			if (taskInfos.build.index !== -1) {
340
				let name = tasks[taskInfos.build.index];
J
Johannes Rieken 已提交
341
				this._stdout.push(nls.localize('TaskSystemDetector.buildTaskDetected', 'Build task named \'{0}\' detected.', name));
E
Erich Gamma 已提交
342
				taskConfigs.push({
343
					taskName: name,
E
Erich Gamma 已提交
344
					args: [],
345
					group: Tasks.TaskGroup.Build,
E
Erich Gamma 已提交
346 347 348 349
					problemMatcher: problemMatchers
				});
			}
			if (taskInfos.test.index !== -1) {
350
				let name = tasks[taskInfos.test.index];
J
Johannes Rieken 已提交
351
				this._stdout.push(nls.localize('TaskSystemDetector.testTaskDetected', 'Test task named \'{0}\' detected.', name));
E
Erich Gamma 已提交
352
				taskConfigs.push({
353
					taskName: name,
E
Erich Gamma 已提交
354
					args: [],
355
					group: Tasks.TaskGroup.Test,
E
Erich Gamma 已提交
356 357 358 359 360 361
				});
			}
		}
		return taskConfigs;
	}

J
Johannes Rieken 已提交
362
	private testBuild(taskInfo: TaskInfo, taskName: string, index: number): void {
E
Erich Gamma 已提交
363
		if (taskName === build) {
364 365
			taskInfo.index = index;
			taskInfo.exact = 4;
366
		} else if ((taskName.startsWith(build) || taskName.endsWith(build)) && taskInfo.exact < 4) {
E
Erich Gamma 已提交
367 368
			taskInfo.index = index;
			taskInfo.exact = 3;
369
		} else if (taskName.indexOf(build) !== -1 && taskInfo.exact < 3) {
E
Erich Gamma 已提交
370 371
			taskInfo.index = index;
			taskInfo.exact = 2;
372
		} else if (taskName === defaultValue && taskInfo.exact < 2) {
E
Erich Gamma 已提交
373 374 375 376 377
			taskInfo.index = index;
			taskInfo.exact = 1;
		}
	}

J
Johannes Rieken 已提交
378
	private testTest(taskInfo: TaskInfo, taskName: string, index: number): void {
E
Erich Gamma 已提交
379 380 381
		if (taskName === test) {
			taskInfo.index = index;
			taskInfo.exact = 3;
382
		} else if ((taskName.startsWith(test) || taskName.endsWith(test)) && taskInfo.exact < 3) {
E
Erich Gamma 已提交
383 384 385 386 387 388 389 390
			taskInfo.index = index;
			taskInfo.exact = 2;
		} else if (taskName.indexOf(test) !== -1 && taskInfo.exact < 2) {
			taskInfo.index = index;
			taskInfo.exact = 1;
		}
	}
}