You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
449 lines
12 KiB
449 lines
12 KiB
let EventEmitter = require('events').EventEmitter; |
|
let async = require('async'); |
|
let chalk = require('chalk'); |
|
// 'rule' module is required at the bottom because circular deps |
|
|
|
// Used for task value, so better not to use |
|
// null, since value should be unset/uninitialized |
|
let UNDEFINED_VALUE; |
|
|
|
const ROOT_TASK_NAME = '__rootTask__'; |
|
const POLLING_INTERVAL = 100; |
|
|
|
// Parse any positional args attached to the task-name |
|
function parsePrereqName(name) { |
|
let taskArr = name.split('['); |
|
let taskName = taskArr[0]; |
|
let taskArgs = []; |
|
if (taskArr[1]) { |
|
taskArgs = taskArr[1].replace(/\]$/, ''); |
|
taskArgs = taskArgs.split(','); |
|
} |
|
return { |
|
name: taskName, |
|
args: taskArgs |
|
}; |
|
} |
|
|
|
/** |
|
@name jake.Task |
|
@class |
|
@extends EventEmitter |
|
@description A Jake Task |
|
|
|
@param {String} name The name of the Task |
|
@param {Array} [prereqs] Prerequisites to be run before this task |
|
@param {Function} [action] The action to perform for this task |
|
@param {Object} [opts] |
|
@param {Array} [opts.asyc=false] Perform this task asynchronously. |
|
If you flag a task with this option, you must call the global |
|
`complete` method inside the task's action, for execution to proceed |
|
to the next task. |
|
*/ |
|
class Task extends EventEmitter { |
|
|
|
constructor(name, prereqs, action, options) { |
|
// EventEmitter ctor takes no args |
|
super(); |
|
|
|
if (name.indexOf(':') > -1) { |
|
throw new Error('Task name cannot include a colon. It is used internally as namespace delimiter.'); |
|
} |
|
let opts = options || {}; |
|
|
|
this._currentPrereqIndex = 0; |
|
this._internal = false; |
|
this._skipped = false; |
|
|
|
this.name = name; |
|
this.prereqs = prereqs; |
|
this.action = action; |
|
this.async = false; |
|
this.taskStatus = Task.runStatuses.UNSTARTED; |
|
this.description = null; |
|
this.args = []; |
|
this.value = UNDEFINED_VALUE; |
|
this.concurrency = 1; |
|
this.startTime = null; |
|
this.endTime = null; |
|
this.directory = null; |
|
this.namespace = null; |
|
|
|
// Support legacy async-flag -- if not explicitly passed or falsy, will |
|
// be set to empty-object |
|
if (typeof opts == 'boolean' && opts === true) { |
|
this.async = true; |
|
} |
|
else { |
|
if (opts.async) { |
|
this.async = true; |
|
} |
|
if (opts.concurrency) { |
|
this.concurrency = opts.concurrency; |
|
} |
|
} |
|
|
|
//Do a test on self dependencies for this task |
|
if(Array.isArray(this.prereqs) && this.prereqs.indexOf(this.name) !== -1) { |
|
throw new Error("Cannot use prereq " + this.name + " as a dependency of itself"); |
|
} |
|
} |
|
|
|
get fullName() { |
|
return this._getFullName(); |
|
} |
|
|
|
get params() { |
|
return this._getParams(); |
|
} |
|
|
|
_initInvocationChain() { |
|
// Legacy global invocation chain |
|
jake._invocationChain.push(this); |
|
|
|
// New root chain |
|
if (!this._invocationChain) { |
|
this._invocationChainRoot = true; |
|
this._invocationChain = []; |
|
if (jake.currentRunningTask) { |
|
jake.currentRunningTask._waitForChains = jake.currentRunningTask._waitForChains || []; |
|
jake.currentRunningTask._waitForChains.push(this._invocationChain); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
@name jake.Task#invoke |
|
@function |
|
@description Runs prerequisites, then this task. If the task has already |
|
been run, will not run the task again. |
|
*/ |
|
invoke() { |
|
this._initInvocationChain(); |
|
|
|
this.args = Array.prototype.slice.call(arguments); |
|
this.reenabled = false |
|
this.runPrereqs(); |
|
} |
|
|
|
/** |
|
@name jake.Task#execute |
|
@function |
|
@description Run only this task, without prereqs. If the task has already |
|
been run, *will* run the task again. |
|
*/ |
|
execute() { |
|
this._initInvocationChain(); |
|
|
|
this.args = Array.prototype.slice.call(arguments); |
|
this.reenable(); |
|
this.reenabled = true |
|
this.run(); |
|
} |
|
|
|
runPrereqs() { |
|
if (this.prereqs && this.prereqs.length) { |
|
|
|
if (this.concurrency > 1) { |
|
async.eachLimit(this.prereqs, this.concurrency, |
|
|
|
(name, cb) => { |
|
let parsed = parsePrereqName(name); |
|
|
|
let prereq = this.namespace.resolveTask(parsed.name) || |
|
jake.attemptRule(name, this.namespace, 0) || |
|
jake.createPlaceholderFileTask(name, this.namespace); |
|
|
|
if (!prereq) { |
|
throw new Error('Unknown task "' + name + '"'); |
|
} |
|
|
|
//Test for circular invocation |
|
if(prereq === this) { |
|
setImmediate(function () { |
|
cb(new Error("Cannot use prereq " + prereq.name + " as a dependency of itself")); |
|
}); |
|
} |
|
|
|
if (prereq.taskStatus == Task.runStatuses.DONE) { |
|
//prereq already done, return |
|
setImmediate(cb); |
|
} |
|
else { |
|
//wait for complete before calling cb |
|
prereq.once('_done', () => { |
|
prereq.removeAllListeners('_done'); |
|
setImmediate(cb); |
|
}); |
|
// Start the prereq if we are the first to encounter it |
|
if (prereq.taskStatus === Task.runStatuses.UNSTARTED) { |
|
prereq.taskStatus = Task.runStatuses.STARTED; |
|
prereq.invoke.apply(prereq, parsed.args); |
|
} |
|
} |
|
}, |
|
|
|
(err) => { |
|
//async callback is called after all prereqs have run. |
|
if (err) { |
|
throw err; |
|
} |
|
else { |
|
setImmediate(this.run.bind(this)); |
|
} |
|
} |
|
); |
|
} |
|
else { |
|
setImmediate(this.nextPrereq.bind(this)); |
|
} |
|
} |
|
else { |
|
setImmediate(this.run.bind(this)); |
|
} |
|
} |
|
|
|
nextPrereq() { |
|
let self = this; |
|
let index = this._currentPrereqIndex; |
|
let name = this.prereqs[index]; |
|
let prereq; |
|
let parsed; |
|
|
|
if (name) { |
|
|
|
parsed = parsePrereqName(name); |
|
|
|
prereq = this.namespace.resolveTask(parsed.name) || |
|
jake.attemptRule(name, this.namespace, 0) || |
|
jake.createPlaceholderFileTask(name, this.namespace); |
|
|
|
if (!prereq) { |
|
throw new Error('Unknown task "' + name + '"'); |
|
} |
|
|
|
// Do when done |
|
if (prereq.taskStatus == Task.runStatuses.DONE) { |
|
self.handlePrereqDone(prereq); |
|
} |
|
else { |
|
prereq.once('_done', () => { |
|
this.handlePrereqDone(prereq); |
|
prereq.removeAllListeners('_done'); |
|
}); |
|
if (prereq.taskStatus == Task.runStatuses.UNSTARTED) { |
|
prereq.taskStatus = Task.runStatuses.STARTED; |
|
prereq._invocationChain = this._invocationChain; |
|
prereq.invoke.apply(prereq, parsed.args); |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
@name jake.Task#reenable |
|
@function |
|
@description Reenables a task so that it can be run again. |
|
*/ |
|
reenable(deep) { |
|
let prereqs; |
|
let prereq; |
|
this._skipped = false; |
|
this.taskStatus = Task.runStatuses.UNSTARTED; |
|
this.value = UNDEFINED_VALUE; |
|
if (deep && this.prereqs) { |
|
prereqs = this.prereqs; |
|
for (let i = 0, ii = prereqs.length; i < ii; i++) { |
|
prereq = jake.Task[prereqs[i]]; |
|
if (prereq) { |
|
prereq.reenable(deep); |
|
} |
|
} |
|
} |
|
} |
|
|
|
handlePrereqDone(prereq) { |
|
this._currentPrereqIndex++; |
|
if (this._currentPrereqIndex < this.prereqs.length) { |
|
setImmediate(this.nextPrereq.bind(this)); |
|
} |
|
else { |
|
setImmediate(this.run.bind(this)); |
|
} |
|
} |
|
|
|
isNeeded() { |
|
let needed = true; |
|
if (this.taskStatus == Task.runStatuses.DONE) { |
|
needed = false; |
|
} |
|
return needed; |
|
} |
|
|
|
run() { |
|
let val, previous; |
|
let hasAction = typeof this.action == 'function'; |
|
|
|
if (!this.isNeeded()) { |
|
this.emit('skip'); |
|
this.emit('_done'); |
|
} |
|
else { |
|
if (this._invocationChain.length) { |
|
previous = this._invocationChain[this._invocationChain.length - 1]; |
|
// 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 |
|
if (!(this.reenabled && previous == this)) { |
|
if (previous.taskStatus != Task.runStatuses.DONE) { |
|
let now = (new Date()).getTime(); |
|
if (now - this.startTime > jake._taskTimeout) { |
|
return jake.fail(`Timed out waiting for task: ${previous.name} with status of ${previous.taskStatus}`); |
|
} |
|
setTimeout(this.run.bind(this), POLLING_INTERVAL); |
|
return; |
|
} |
|
} |
|
} |
|
if (!(this.reenabled && previous == this)) { |
|
this._invocationChain.push(this); |
|
} |
|
|
|
if (!(this._internal || jake.program.opts.quiet)) { |
|
console.log("Starting '" + chalk.green(this.fullName) + "'..."); |
|
} |
|
|
|
this.startTime = (new Date()).getTime(); |
|
this.emit('start'); |
|
|
|
jake.currentRunningTask = this; |
|
|
|
if (hasAction) { |
|
try { |
|
if (this.directory) { |
|
process.chdir(this.directory); |
|
} |
|
|
|
val = this.action.apply(this, this.args); |
|
|
|
if (typeof val == 'object' && typeof val.then == 'function') { |
|
this.async = true; |
|
|
|
val.then( |
|
(result) => { |
|
setImmediate(() => { |
|
this.complete(result); |
|
}); |
|
}, |
|
(err) => { |
|
setImmediate(() => { |
|
this.errorOut(err); |
|
}); |
|
}); |
|
} |
|
} |
|
catch (err) { |
|
this.errorOut(err); |
|
return; // Bail out, not complete |
|
} |
|
} |
|
|
|
if (!(hasAction && this.async)) { |
|
setImmediate(() => { |
|
this.complete(val); |
|
}); |
|
} |
|
} |
|
} |
|
|
|
errorOut(err) { |
|
this.taskStatus = Task.runStatuses.ERROR; |
|
this._invocationChain.chainStatus = Task.runStatuses.ERROR; |
|
this.emit('error', err); |
|
} |
|
|
|
complete(val) { |
|
|
|
if (Array.isArray(this._waitForChains)) { |
|
let stillWaiting = this._waitForChains.some((chain) => { |
|
return !(chain.chainStatus == Task.runStatuses.DONE || |
|
chain.chainStatus == Task.runStatuses.ERROR); |
|
}); |
|
if (stillWaiting) { |
|
let now = (new Date()).getTime(); |
|
let elapsed = now - this.startTime; |
|
if (elapsed > jake._taskTimeout) { |
|
return jake.fail(`Timed out waiting for task: ${this.name} with status of ${this.taskStatus}. Elapsed: ${elapsed}`); |
|
} |
|
setTimeout(() => { |
|
this.complete(val); |
|
}, POLLING_INTERVAL); |
|
return; |
|
} |
|
} |
|
|
|
jake._invocationChain.splice(jake._invocationChain.indexOf(this), 1); |
|
|
|
if (this._invocationChainRoot) { |
|
this._invocationChain.chainStatus = Task.runStatuses.DONE; |
|
} |
|
|
|
this._currentPrereqIndex = 0; |
|
|
|
// If 'complete' getting called because task has been |
|
// run already, value will not be passed -- leave in place |
|
if (!this._skipped) { |
|
this.taskStatus = Task.runStatuses.DONE; |
|
this.value = val; |
|
|
|
this.emit('complete', this.value); |
|
this.emit('_done'); |
|
|
|
this.endTime = (new Date()).getTime(); |
|
let taskTime = this.endTime - this.startTime; |
|
|
|
if (!(this._internal || jake.program.opts.quiet)) { |
|
console.log("Finished '" + chalk.green(this.fullName) + "' after " + chalk.magenta(taskTime + ' ms')); |
|
} |
|
|
|
} |
|
} |
|
|
|
_getFullName() { |
|
let ns = this.namespace; |
|
let path = (ns && ns.path) || ''; |
|
path = (path && path.split(':')) || []; |
|
if (this.namespace !== jake.defaultNamespace) { |
|
path.push(this.namespace.name); |
|
} |
|
path.push(this.name); |
|
return path.join(':'); |
|
} |
|
|
|
_getParams() { |
|
if (!this.action) return ""; |
|
let params = new RegExp('(?:'+this.action.name+'\\s*|^)\\s*\\((.*?)\\)').exec(this.action.toString().replace(/\n/g, ''))[1].replace(/\/\*.*?\*\//g, '').replace(/ /g, ''); |
|
return params; |
|
} |
|
|
|
static getBaseNamespacePath(fullName) { |
|
return fullName.split(':').slice(0, -1).join(':'); |
|
} |
|
|
|
static getBaseTaskName(fullName) { |
|
return fullName.split(':').pop(); |
|
} |
|
} |
|
|
|
Task.runStatuses = { |
|
UNSTARTED: 'unstarted', |
|
DONE: 'done', |
|
STARTED: 'started', |
|
ERROR: 'error' |
|
}; |
|
|
|
Task.ROOT_TASK_NAME = ROOT_TASK_NAME; |
|
|
|
exports.Task = Task; |
|
|
|
// Required here because circular deps |
|
require('../rule'); |
|
|
|
|