1let EventEmitter = require('events').EventEmitter;
2let async = require('async');
3let chalk = require('chalk');
4// 'rule' module is required at the bottom because circular deps
5
6// Used for task value, so better not to use
7// null, since value should be unset/uninitialized
8let UNDEFINED_VALUE;
9
10const ROOT_TASK_NAME = '__rootTask__';
11const POLLING_INTERVAL = 100;
12
13// Parse any positional args attached to the task-name
14function parsePrereqName(name) {
15  let taskArr = name.split('[');
16  let taskName = taskArr[0];
17  let taskArgs = [];
18  if (taskArr[1]) {
19    taskArgs = taskArr[1].replace(/\]$/, '');
20    taskArgs = taskArgs.split(',');
21  }
22  return {
23    name: taskName,
24    args: taskArgs
25  };
26}
27
28/**
29  @name jake.Task
30  @class
31  @extends EventEmitter
32  @description A Jake Task
33
34  @param {String} name The name of the Task
35  @param {Array} [prereqs] Prerequisites to be run before this task
36  @param {Function} [action] The action to perform for this task
37  @param {Object} [opts]
38    @param {Array} [opts.asyc=false] Perform this task asynchronously.
39    If you flag a task with this option, you must call the global
40    `complete` method inside the task's action, for execution to proceed
41    to the next task.
42 */
43class Task extends EventEmitter {
44
45  constructor(name, prereqs, action, options) {
46    // EventEmitter ctor takes no args
47    super();
48
49    if (name.indexOf(':') > -1) {
50      throw new Error('Task name cannot include a colon. It is used internally as namespace delimiter.');
51    }
52    let opts = options || {};
53
54    this._currentPrereqIndex = 0;
55    this._internal = false;
56    this._skipped = false;
57
58    this.name = name;
59    this.prereqs = prereqs;
60    this.action = action;
61    this.async = false;
62    this.taskStatus = Task.runStatuses.UNSTARTED;
63    this.description = null;
64    this.args = [];
65    this.value = UNDEFINED_VALUE;
66    this.concurrency = 1;
67    this.startTime = null;
68    this.endTime = null;
69    this.directory = null;
70    this.namespace = null;
71
72    // Support legacy async-flag -- if not explicitly passed or falsy, will
73    // be set to empty-object
74    if (typeof opts == 'boolean' && opts === true) {
75      this.async = true;
76    }
77    else {
78      if (opts.async) {
79        this.async = true;
80      }
81      if (opts.concurrency) {
82        this.concurrency = opts.concurrency;
83      }
84    }
85
86    //Do a test on self dependencies for this task
87    if(Array.isArray(this.prereqs) && this.prereqs.indexOf(this.name) !== -1) {
88      throw new Error("Cannot use prereq " + this.name + " as a dependency of itself");
89    }
90  }
91
92  get fullName() {
93    return this._getFullName();
94  }
95
96  get params() {
97    return this._getParams();
98  }
99
100  _initInvocationChain() {
101    // Legacy global invocation chain
102    jake._invocationChain.push(this);
103
104    // New root chain
105    if (!this._invocationChain) {
106      this._invocationChainRoot = true;
107      this._invocationChain = [];
108      if (jake.currentRunningTask) {
109        jake.currentRunningTask._waitForChains = jake.currentRunningTask._waitForChains || [];
110        jake.currentRunningTask._waitForChains.push(this._invocationChain);
111      }
112    }
113  }
114
115  /**
116    @name jake.Task#invoke
117    @function
118    @description Runs prerequisites, then this task. If the task has already
119    been run, will not run the task again.
120   */
121  invoke() {
122    this._initInvocationChain();
123
124    this.args = Array.prototype.slice.call(arguments);
125    this.reenabled = false;
126    this.runPrereqs();
127  }
128
129  /**
130    @name jake.Task#execute
131    @function
132    @description Run only this task, without prereqs. If the task has already
133    been run, *will* run the task again.
134   */
135  execute() {
136    this._initInvocationChain();
137
138    this.args = Array.prototype.slice.call(arguments);
139    this.reenable();
140    this.reenabled = true;
141    this.run();
142  }
143
144  runPrereqs() {
145    if (this.prereqs && this.prereqs.length) {
146
147      if (this.concurrency > 1) {
148        async.eachLimit(this.prereqs, this.concurrency,
149
150          (name, cb) => {
151            let parsed = parsePrereqName(name);
152
153            let prereq = this.namespace.resolveTask(parsed.name) ||
154          jake.attemptRule(name, this.namespace, 0) ||
155          jake.createPlaceholderFileTask(name, this.namespace);
156
157            if (!prereq) {
158              throw new Error('Unknown task "' + name + '"');
159            }
160
161            //Test for circular invocation
162            if(prereq === this) {
163              setImmediate(function () {
164                cb(new Error("Cannot use prereq " + prereq.name + " as a dependency of itself"));
165              });
166            }
167
168            if (prereq.taskStatus == Task.runStatuses.DONE) {
169            //prereq already done, return
170              setImmediate(cb);
171            }
172            else {
173            //wait for complete before calling cb
174              prereq.once('_done', () => {
175                prereq.removeAllListeners('_done');
176                setImmediate(cb);
177              });
178              // Start the prereq if we are the first to encounter it
179              if (prereq.taskStatus === Task.runStatuses.UNSTARTED) {
180                prereq.taskStatus = Task.runStatuses.STARTED;
181                prereq.invoke.apply(prereq, parsed.args);
182              }
183            }
184          },
185
186          (err) => {
187          //async callback is called after all prereqs have run.
188            if (err) {
189              throw err;
190            }
191            else {
192              setImmediate(this.run.bind(this));
193            }
194          }
195        );
196      }
197      else {
198        setImmediate(this.nextPrereq.bind(this));
199      }
200    }
201    else {
202      setImmediate(this.run.bind(this));
203    }
204  }
205
206  nextPrereq() {
207    let self = this;
208    let index = this._currentPrereqIndex;
209    let name = this.prereqs[index];
210    let prereq;
211    let parsed;
212
213    if (name) {
214
215      parsed = parsePrereqName(name);
216
217      prereq = this.namespace.resolveTask(parsed.name) ||
218          jake.attemptRule(name, this.namespace, 0) ||
219          jake.createPlaceholderFileTask(name, this.namespace);
220
221      if (!prereq) {
222        throw new Error('Unknown task "' + name + '"');
223      }
224
225      // Do when done
226      if (prereq.taskStatus == Task.runStatuses.DONE) {
227        self.handlePrereqDone(prereq);
228      }
229      else {
230        prereq.once('_done', () => {
231          this.handlePrereqDone(prereq);
232          prereq.removeAllListeners('_done');
233        });
234        if (prereq.taskStatus == Task.runStatuses.UNSTARTED) {
235          prereq.taskStatus = Task.runStatuses.STARTED;
236          prereq._invocationChain = this._invocationChain;
237          prereq.invoke.apply(prereq, parsed.args);
238        }
239      }
240    }
241  }
242
243  /**
244    @name jake.Task#reenable
245    @function
246    @description Reenables a task so that it can be run again.
247   */
248  reenable(deep) {
249    let prereqs;
250    let prereq;
251    this._skipped = false;
252    this.taskStatus = Task.runStatuses.UNSTARTED;
253    this.value = UNDEFINED_VALUE;
254    if (deep && this.prereqs) {
255      prereqs = this.prereqs;
256      for (let i = 0, ii = prereqs.length; i < ii; i++) {
257        prereq = jake.Task[prereqs[i]];
258        if (prereq) {
259          prereq.reenable(deep);
260        }
261      }
262    }
263  }
264
265  handlePrereqDone(prereq) {
266    this._currentPrereqIndex++;
267    if (this._currentPrereqIndex < this.prereqs.length) {
268      setImmediate(this.nextPrereq.bind(this));
269    }
270    else {
271      setImmediate(this.run.bind(this));
272    }
273  }
274
275  isNeeded() {
276    let needed = true;
277    if (this.taskStatus == Task.runStatuses.DONE) {
278      needed = false;
279    }
280    return needed;
281  }
282
283  run() {
284    let val, previous;
285    let hasAction = typeof this.action == 'function';
286
287    if (!this.isNeeded()) {
288      this.emit('skip');
289      this.emit('_done');
290    }
291    else {
292      if (this._invocationChain.length) {
293        previous = this._invocationChain[this._invocationChain.length - 1];
294        // If this task is repeating and its previous is equal to this, don't check its status because it was set to UNSTARTED by the reenable() method
295        if (!(this.reenabled && previous == this)) {
296          if (previous.taskStatus != Task.runStatuses.DONE) {
297            let now = (new Date()).getTime();
298            if (now - this.startTime > jake._taskTimeout) {
299              return jake.fail(`Timed out waiting for task: ${previous.name} with status of ${previous.taskStatus}`);
300            }
301            setTimeout(this.run.bind(this), POLLING_INTERVAL);
302            return;
303          }
304        }
305      }
306      if (!(this.reenabled && previous == this)) {
307        this._invocationChain.push(this);
308      }
309
310      if (!(this._internal || jake.program.opts.quiet)) {
311        console.log("Starting '" + chalk.green(this.fullName) + "'...");
312      }
313
314      this.startTime = (new Date()).getTime();
315      this.emit('start');
316
317      jake.currentRunningTask = this;
318
319      if (hasAction) {
320        try {
321          if (this.directory) {
322            process.chdir(this.directory);
323          }
324
325          val = this.action.apply(this, this.args);
326
327          if (typeof val == 'object' && typeof val.then == 'function') {
328            this.async = true;
329
330            val.then(
331              (result) => {
332                setImmediate(() => {
333                  this.complete(result);
334                });
335              },
336              (err) => {
337                setImmediate(() => {
338                  this.errorOut(err);
339                });
340              });
341          }
342        }
343        catch (err) {
344          this.errorOut(err);
345          return; // Bail out, not complete
346        }
347      }
348
349      if (!(hasAction && this.async)) {
350        setImmediate(() => {
351          this.complete(val);
352        });
353      }
354    }
355  }
356
357  errorOut(err) {
358    this.taskStatus = Task.runStatuses.ERROR;
359    this._invocationChain.chainStatus = Task.runStatuses.ERROR;
360    this.emit('error', err);
361  }
362
363  complete(val) {
364
365    if (Array.isArray(this._waitForChains)) {
366      let stillWaiting = this._waitForChains.some((chain) => {
367        return !(chain.chainStatus == Task.runStatuses.DONE ||
368              chain.chainStatus == Task.runStatuses.ERROR);
369      });
370      if (stillWaiting) {
371        let now = (new Date()).getTime();
372        let elapsed = now - this.startTime;
373        if (elapsed > jake._taskTimeout) {
374          return jake.fail(`Timed out waiting for task: ${this.name} with status of ${this.taskStatus}. Elapsed: ${elapsed}`);
375        }
376        setTimeout(() => {
377          this.complete(val);
378        }, POLLING_INTERVAL);
379        return;
380      }
381    }
382
383    jake._invocationChain.splice(jake._invocationChain.indexOf(this), 1);
384
385    if (this._invocationChainRoot) {
386      this._invocationChain.chainStatus = Task.runStatuses.DONE;
387    }
388
389    this._currentPrereqIndex = 0;
390
391    // If 'complete' getting called because task has been
392    // run already, value will not be passed -- leave in place
393    if (!this._skipped) {
394      this.taskStatus = Task.runStatuses.DONE;
395      this.value = val;
396
397      this.emit('complete', this.value);
398      this.emit('_done');
399
400      this.endTime = (new Date()).getTime();
401      let taskTime = this.endTime - this.startTime;
402
403      if (!(this._internal || jake.program.opts.quiet)) {
404        console.log("Finished '" + chalk.green(this.fullName) + "' after " + chalk.magenta(taskTime + ' ms'));
405      }
406
407    }
408  }
409
410  _getFullName() {
411    let ns = this.namespace;
412    let path = (ns && ns.path) || '';
413    path = (path && path.split(':')) || [];
414    if (this.namespace !== jake.defaultNamespace) {
415      path.push(this.namespace.name);
416    }
417    path.push(this.name);
418    return path.join(':');
419  }
420
421  _getParams() {
422    if (!this.action) return "";
423    let params = (new RegExp('(?:'+this.action.name+'\\s*|^)\\s*\\((.*?)\\)').exec(this.action.toString().replace(/\n/g, '')) || [''])[1].replace(/\/\*.*?\*\//g, '').replace(/ /g, '');
424    return params;
425  }
426
427  static getBaseNamespacePath(fullName) {
428    return fullName.split(':').slice(0, -1).join(':');
429  }
430
431  static getBaseTaskName(fullName) {
432    return fullName.split(':').pop();
433  }
434}
435
436Task.runStatuses = {
437  UNSTARTED: 'unstarted',
438  DONE: 'done',
439  STARTED: 'started',
440  ERROR: 'error'
441};
442
443Task.ROOT_TASK_NAME = ROOT_TASK_NAME;
444
445exports.Task = Task;
446
447// Required here because circular deps
448require('../rule');
449
450