/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as Collections from 'vs/base/common/collections'; import * as Objects from 'vs/base/common/objects'; import * as Path from 'vs/base/common/path'; import { CommandOptions, ErrorData, Source } from 'vs/base/common/processes'; import * as Strings from 'vs/base/common/strings'; 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'; import * as TaskConfig from '../common/taskConfiguration'; const build = 'build'; const test = 'test'; const defaultValue = 'default'; interface TaskInfo { index: number; exact: number; } interface TaskInfos { build: TaskInfo; test: TaskInfo; } interface TaskDetectorMatcher { init(): void; match(tasks: string[], line: string): void; } interface DetectorConfig { matcher: TaskDetectorMatcher; arg: string; } class RegexpTaskMatcher implements TaskDetectorMatcher { private regexp: RegExp; constructor(regExp: RegExp) { this.regexp = regExp; } init() { } match(tasks: string[], line: string): void { let matches = this.regexp.exec(line); if (matches && matches.length > 0) { tasks.push(matches[1]); } } } class GruntTaskMatcher implements TaskDetectorMatcher { private tasksStart!: boolean; private tasksEnd!: boolean; private descriptionOffset!: number | null; init() { this.tasksStart = false; this.tasksEnd = false; this.descriptionOffset = null; } match(tasks: string[], line: string): void { // 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; } } else if (this.tasksStart && !this.tasksEnd) { if (line.indexOf('Tasks run in the order specified') === 0) { this.tasksEnd = true; } else { if (this.descriptionOffset === null) { const match = line.match(/\S \S/); if (match) { this.descriptionOffset = (match.index || 0) + 1; } else { this.descriptionOffset = 0; } } let taskName = line.substr(0, this.descriptionOffset).trim(); if (taskName.length > 0) { tasks.push(taskName); } } } } } export interface DetectorResult { config: TaskConfig.ExternalTaskRunnerConfiguration | null; stdout: string[]; stderr: string[]; } export class ProcessRunnerDetector { private static Version: string = '0.1.0'; private static SupportedRunners: Collections.IStringDictionary = { 'gulp': true, 'jake': true, 'grunt': true }; private static TaskMatchers: Collections.IStringDictionary = { '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]; } private static detectorConfig(runner: string): DetectorConfig { return ProcessRunnerDetector.TaskMatchers[runner]; } private static DefaultProblemMatchers: string[] = ['$lessCompile', '$tsc', '$jshint']; private fileService: IFileService; private contextService: IWorkspaceContextService; private configurationResolverService: IConfigurationResolverService; private taskConfiguration: TaskConfig.ExternalTaskRunnerConfiguration | null; private _workspaceRoot: IWorkspaceFolder; private _stderr: string[]; private _stdout: string[]; private _cwd: string; constructor(workspaceFolder: IWorkspaceFolder, fileService: IFileService, contextService: IWorkspaceContextService, configurationResolverService: IConfigurationResolverService, config: TaskConfig.ExternalTaskRunnerConfiguration | null = null) { this.fileService = fileService; this.contextService = contextService; this.configurationResolverService = configurationResolverService; this.taskConfiguration = config; this._workspaceRoot = workspaceFolder; this._stderr = []; this._stdout = []; this._cwd = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? Path.normalize(this._workspaceRoot.uri.fsPath) : ''; } public get stderr(): string[] { return this._stderr; } public get stdout(): string[] { return this._stdout; } public detect(list: boolean = false, detectSpecific?: string): Promise { let commandExecutable: string; if (this.taskConfiguration && this.taskConfiguration.command && (commandExecutable = TaskConfig.CommandString.value(this.taskConfiguration.command)) && ProcessRunnerDetector.supports(commandExecutable)) { let config = ProcessRunnerDetector.detectorConfig(commandExecutable); let args = (this.taskConfiguration.args || []).concat(config.arg); let options: CommandOptions = this.taskConfiguration.options ? this.resolveCommandOptions(this._workspaceRoot, this.taskConfiguration.options) : { cwd: this._cwd }; let isShellCommand = !!this.taskConfiguration.isShellCommand; return Promise.resolve(this.runDetection( new LineProcess(commandExecutable, this.configurationResolverService.resolve(this._workspaceRoot, args.map(a => TaskConfig.CommandString.value(a))), isShellCommand, options), commandExecutable, isShellCommand, config.matcher, ProcessRunnerDetector.DefaultProblemMatchers, list)); } else { if (detectSpecific) { let detectorPromise: Promise; if ('gulp' === detectSpecific) { detectorPromise = this.tryDetectGulp(this._workspaceRoot, list); } else if ('jake' === detectSpecific) { detectorPromise = this.tryDetectJake(this._workspaceRoot, list); } else if ('grunt' === detectSpecific) { detectorPromise = this.tryDetectGrunt(this._workspaceRoot, list); } else { throw new Error('Unknown detector type'); } return detectorPromise.then((value) => { if (value) { return value; } else { return { config: null, stdout: this.stdout, stderr: this.stderr }; } }); } else { return this.tryDetectGulp(this._workspaceRoot, list).then((value) => { if (value) { return value; } return this.tryDetectJake(this._workspaceRoot, list).then((value) => { if (value) { return value; } return this.tryDetectGrunt(this._workspaceRoot, list).then((value) => { if (value) { return value; } return { config: null, stdout: this.stdout, stderr: this.stderr }; }); }); }); } } } private resolveCommandOptions(workspaceFolder: IWorkspaceFolder, options: CommandOptions): CommandOptions { // TODO@Dirk adopt new configuration resolver service https://github.com/microsoft/vscode/issues/31365 let result = Objects.deepClone(options); if (result.cwd) { result.cwd = this.configurationResolverService.resolve(workspaceFolder, result.cwd); } if (result.env) { result.env = this.configurationResolverService.resolve(workspaceFolder, result.env); } return result; } private tryDetectGulp(workspaceFolder: IWorkspaceFolder, list: boolean): Promise { return Promise.resolve(this.fileService.resolve(workspaceFolder.toResource('gulpfile.js'))).then((stat) => { // TODO@Dirk (https://github.com/microsoft/vscode/issues/29454) let config = ProcessRunnerDetector.detectorConfig('gulp'); let process = new LineProcess('gulp', [config.arg, '--no-color'], true, { cwd: this._cwd }); return this.runDetection(process, 'gulp', true, config.matcher, ProcessRunnerDetector.DefaultProblemMatchers, list); }, (err: any) => { return null; }); } private tryDetectGrunt(workspaceFolder: IWorkspaceFolder, list: boolean): Promise { return Promise.resolve(this.fileService.resolve(workspaceFolder.toResource('Gruntfile.js'))).then((stat) => { // TODO@Dirk (https://github.com/microsoft/vscode/issues/29454) let config = ProcessRunnerDetector.detectorConfig('grunt'); let process = new LineProcess('grunt', [config.arg, '--no-color'], true, { cwd: this._cwd }); return this.runDetection(process, 'grunt', true, config.matcher, ProcessRunnerDetector.DefaultProblemMatchers, list); }, (err: any) => { return null; }); } private tryDetectJake(workspaceFolder: IWorkspaceFolder, list: boolean): Promise { let run = () => { let config = ProcessRunnerDetector.detectorConfig('jake'); let process = new LineProcess('jake', [config.arg], true, { cwd: this._cwd }); return this.runDetection(process, 'jake', true, config.matcher, ProcessRunnerDetector.DefaultProblemMatchers, list); }; return Promise.resolve(this.fileService.resolve(workspaceFolder.toResource('Jakefile'))).then((stat) => { // TODO@Dirk (https://github.com/microsoft/vscode/issues/29454) return run(); }, (err: any) => { return this.fileService.resolve(workspaceFolder.toResource('Jakefile.js')).then((stat) => { // TODO@Dirk (https://github.com/microsoft/vscode/issues/29454) return run(); }, (err: any) => { return null; }); }); } private runDetection(process: LineProcess, command: string, isShellCommand: boolean, matcher: TaskDetectorMatcher, problemMatchers: string[], list: boolean): Promise { let tasks: string[] = []; matcher.init(); const onProgress = (progress: LineData) => { if (progress.source === Source.stderr) { this._stderr.push(progress.line); return; } let line = Strings.removeAnsiEscapeCodes(progress.line); matcher.match(tasks, line); }; return process.start(onProgress).then((success) => { 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?')); } } return { config: null, stdout: this._stdout, stderr: this._stderr }; } let result: TaskConfig.ExternalTaskRunnerConfiguration = { 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); return { config: result, stdout: this._stdout, stderr: this._stderr }; }, (err: ErrorData) => { let error = err.error; if ((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') { this._stderr.push(nls.localize('TaskSystemDetector.noGruntProgram', 'Grunt is not installed on your system. Run npm install -g grunt to install it.')); } } else { this._stderr.push(nls.localize('TaskSystemDetector.noProgram', 'Program {0} was not found. Message is {1}', command, error ? error.message : '')); } return { config: null, stdout: this._stdout, stderr: this._stderr }; }); } private createTaskDescriptions(tasks: string[], problemMatchers: string[], list: boolean): TaskConfig.CustomTask[] { let taskConfigs: TaskConfig.CustomTask[] = []; if (list) { tasks.forEach((task) => { taskConfigs.push({ taskName: task, args: [] }); }); } 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) { let name = tasks[taskInfos.build.index]; this._stdout.push(nls.localize('TaskSystemDetector.buildTaskDetected', 'Build task named \'{0}\' detected.', name)); taskConfigs.push({ taskName: name, args: [], group: Tasks.TaskGroup.Build, problemMatcher: problemMatchers }); } if (taskInfos.test.index !== -1) { let name = tasks[taskInfos.test.index]; this._stdout.push(nls.localize('TaskSystemDetector.testTaskDetected', 'Test task named \'{0}\' detected.', name)); taskConfigs.push({ taskName: name, args: [], group: Tasks.TaskGroup.Test, }); } } return taskConfigs; } private testBuild(taskInfo: TaskInfo, taskName: string, index: number): void { if (taskName === build) { taskInfo.index = index; taskInfo.exact = 4; } else if ((taskName.startsWith(build) || taskName.endsWith(build)) && taskInfo.exact < 4) { taskInfo.index = index; taskInfo.exact = 3; } else if (taskName.indexOf(build) !== -1 && taskInfo.exact < 3) { taskInfo.index = index; taskInfo.exact = 2; } else if (taskName === defaultValue && taskInfo.exact < 2) { taskInfo.index = index; taskInfo.exact = 1; } } private testTest(taskInfo: TaskInfo, taskName: string, index: number): void { if (taskName === test) { taskInfo.index = index; taskInfo.exact = 3; } else if ((taskName.startsWith(test) || taskName.endsWith(test)) && taskInfo.exact < 3) { taskInfo.index = index; taskInfo.exact = 2; } else if (taskName.indexOf(test) !== -1 && taskInfo.exact < 2) { taskInfo.index = index; taskInfo.exact = 1; } } }