123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- 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');
|