task.js 12 KB

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