task.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. var util = require('util') // Native Node util module
  2. , path = require('path')
  3. , EventEmitter = require('events').EventEmitter
  4. , Task
  5. , TaskBase
  6. , utils = require('../utils')
  7. , async = require('async')
  8. , rule; // Lazy-require this at the bottom
  9. var UNDEFINED_VALUE;
  10. /**
  11. @name jake
  12. @namespace jake
  13. */
  14. /**
  15. @name jake.Task
  16. @constructor
  17. @augments EventEmitter
  18. @description A Jake Task
  19. @param {String} name The name of the Task
  20. @param {Array} [prereqs] Prerequisites to be run before this task
  21. @param {Function} [action] The action to perform for this task
  22. @param {Object} [opts]
  23. @param {Array} [opts.asyc=false] Perform this task asynchronously.
  24. If you flag a task with this option, you must call the global
  25. `complete` method inside the task's action, for execution to proceed
  26. to the next task.
  27. */
  28. Task = function () {
  29. // Do constructor-work only on actual instances, not when used
  30. // for inheritance
  31. if (arguments.length) {
  32. this.init.apply(this, arguments);
  33. }
  34. };
  35. util.inherits(Task, EventEmitter);
  36. TaskBase = new (function () {
  37. // Parse any positional args attached to the task-name
  38. var parsePrereqName = function (name) {
  39. var taskArr = name.split('[')
  40. , taskName = taskArr[0]
  41. , taskArgs = [];
  42. if (taskArr[1]) {
  43. taskArgs = taskArr[1].replace(/\]$/, '');
  44. taskArgs = taskArgs.split(',');
  45. }
  46. return {
  47. name: taskName
  48. , args: taskArgs
  49. };
  50. };
  51. /**
  52. @name jake.Task#event:complete
  53. @event
  54. */
  55. this.init = function (name, prereqs, action, options) {
  56. var opts = options || {};
  57. this._currentPrereqIndex = 0;
  58. this.name = name;
  59. this.prereqs = prereqs;
  60. this.action = action;
  61. this.async = false;
  62. this.taskStatus = Task.runStatuses.UNSTARTED;
  63. this.fullName = null;
  64. this.description = null;
  65. this.args = [];
  66. this.value = UNDEFINED_VALUE;
  67. this.namespace = null;
  68. this.parallelLimit = 1;
  69. // Support legacy async-flag -- if not explicitly passed or falsy, will
  70. // be set to empty-object
  71. if (typeof opts == 'boolean' && opts === true) {
  72. this.async = true;
  73. }
  74. else {
  75. if (opts.async) {
  76. this.async = true;
  77. }
  78. if (opts.parallelLimit) {
  79. this.parallelLimit = opts.parallelLimit;
  80. }
  81. }
  82. };
  83. /**
  84. @name jake.Task#invoke
  85. @function
  86. @description Runs prerequisites, then this task. If the task has already
  87. been run, will not run the task again.
  88. */
  89. this.invoke = function () {
  90. jake._invocationChain.push(this);
  91. this.args = Array.prototype.slice.call(arguments);
  92. this.runPrereqs();
  93. };
  94. /**
  95. @name jake.Task#execute
  96. @function
  97. @description Runs prerequisites, then this task. If the task has already
  98. been run, will not run the task again.
  99. */
  100. this.execute = function () {
  101. jake._invocationChain.push(this);
  102. this.args = Array.prototype.slice.call(arguments);
  103. this.reenable();
  104. this.run();
  105. };
  106. this.runPrereqs = function () {
  107. if (this.prereqs && this.prereqs.length) {
  108. if(this.parallelLimit > 1) {
  109. var currenttask = this;
  110. async.eachLimit(currenttask.prereqs,currenttask.parallelLimit,function(name, cb) {
  111. var parsed = parsePrereqName(name);
  112. var prereq = currenttask.namespace.resolveTask(parsed.name) ||
  113. jake.attemptRule(name, currenttask.namespace, 0) ||
  114. jake.createPlaceholderFileTask(name, currenttask.namespace);
  115. if (!prereq) {
  116. throw new Error('Unknown task "' + name + '"');
  117. }
  118. if (prereq.taskStatus === Task.runStatuses.DONE) {
  119. //prereq already done, return
  120. cb();
  121. } else {
  122. //wait for complete before calling cb
  123. prereq.once('complete', function () {
  124. cb();
  125. });
  126. //start te prereq if we are the first to encounter it
  127. if(prereq.taskStatus === Task.runStatuses.UNSTARTED) {
  128. prereq.taskStatus = Task.runStatuses.RUNNING;
  129. prereq.invoke.apply(prereq, parsed.args);
  130. }
  131. }
  132. }, function(err) {
  133. //async callback is called after all prereqs have run.
  134. currenttask.run();
  135. });
  136. } else {
  137. this.nextPrereq();
  138. }
  139. }
  140. else {
  141. this.run();
  142. }
  143. };
  144. this.nextPrereq = function () {
  145. var self = this
  146. , index = this._currentPrereqIndex
  147. , name = this.prereqs[index]
  148. , prereq
  149. , parsed
  150. , filePath
  151. , stats;
  152. if (name) {
  153. parsed = parsePrereqName(name);
  154. prereq = this.namespace.resolveTask(parsed.name) ||
  155. jake.attemptRule(name, this.namespace, 0) ||
  156. jake.createPlaceholderFileTask(name, this.namespace);
  157. if (!prereq) {
  158. throw new Error('Unknown task "' + name + '"');
  159. }
  160. // Do when done
  161. if (prereq.taskStatus === Task.runStatuses.DONE) {
  162. self.handlePrereqComplete(prereq);
  163. } else {
  164. prereq.once('complete', function () {
  165. self.handlePrereqComplete(prereq);
  166. });
  167. if(prereq.taskStatus === Task.runStatuses.UNSTARTED) {
  168. prereq.taskStatus = Task.runStatuses.RUNNING;
  169. prereq.invoke.apply(prereq, parsed.args);
  170. }
  171. }
  172. }
  173. };
  174. /**
  175. @name jake.Task#reenable
  176. @function
  177. @description Reenables a task so that it can be run again.
  178. */
  179. this.reenable = function (deep) {
  180. var prereqs
  181. , prereq;
  182. this.taskStatus = Task.runStatuses.UNSTARTED;
  183. this.value = UNDEFINED_VALUE;
  184. if (deep && this.prereqs) {
  185. prereqs = this.prereqs;
  186. for (var i = 0, ii = prereqs.length; i < ii; i++) {
  187. prereq = jake.Task[prereqs[i]];
  188. if (prereq) {
  189. prereq.reenable(deep);
  190. }
  191. }
  192. }
  193. };
  194. this.handlePrereqComplete = function (prereq) {
  195. var self = this;
  196. this._currentPrereqIndex++;
  197. if (this._currentPrereqIndex < this.prereqs.length) {
  198. setTimeout(function () {
  199. self.nextPrereq();
  200. }, 0);
  201. }
  202. else {
  203. this.run();
  204. }
  205. };
  206. this.isNeeded = function () {
  207. if (this.taskStatus === Task.runStatuses.DONE || typeof this.action != 'function') {
  208. return false;
  209. }
  210. return true;
  211. };
  212. this.run = function () {
  213. var runAction = this.isNeeded()
  214. , val;
  215. if (runAction) {
  216. this.emit('start');
  217. try {
  218. val = this.action.apply(this, this.args);
  219. if (typeof val == 'object' && typeof val.then == 'function') {
  220. this.async = true;
  221. val.then(
  222. function(result) {
  223. setTimeout(function() {
  224. complete(result);
  225. },
  226. 0);
  227. },
  228. function(err) {
  229. setTimeout(function() {
  230. fail(err);
  231. },
  232. 0);
  233. });
  234. }
  235. }
  236. catch (e) {
  237. this.emit('error', e);
  238. return; // Bail out, not complete
  239. }
  240. }
  241. else {
  242. this.emit('skip');
  243. }
  244. if (!(runAction && this.async)) {
  245. this.complete(val);
  246. }
  247. };
  248. this.complete = function (val) {
  249. jake._invocationChain.splice(jake._invocationChain.indexOf(this),1);
  250. this._currentPrereqIndex = 0;
  251. this.taskStatus = Task.runStatuses.DONE;
  252. // If 'complete' getting called because task has been
  253. // run already, value will not be passed -- leave in place
  254. if (typeof val != 'undefined') {
  255. this.value = val;
  256. }
  257. this.emit('complete', this.value);
  258. };
  259. })();
  260. utils.mixin(Task.prototype, TaskBase);
  261. Task.getBaseNamespacePath = function (fullName) {
  262. return fullName.split(':').slice(0, -1).join(':');
  263. };
  264. Task.getBaseTaskName = function (fullName) {
  265. return fullName.split(':').pop();
  266. };
  267. //The task is in one of three states
  268. Task.runStatuses = {UNSTARTED: 'unstarted', DONE: 'done', STARTED: 'started'};
  269. exports.Task = Task;
  270. // Lazy-require
  271. rule = require('../rule');