run-tasks.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /**
  2. * @module run-tasks-in-parallel
  3. * @author Toru Nagashima
  4. * @copyright 2015 Toru Nagashima. All rights reserved.
  5. * See LICENSE file in root directory for full license.
  6. */
  7. 'use strict'
  8. // ------------------------------------------------------------------------------
  9. // Requirements
  10. // ------------------------------------------------------------------------------
  11. const MemoryStream = require('memorystream')
  12. const NpmRunAllError = require('./npm-run-all-error')
  13. const runTask = require('./run-task')
  14. // ------------------------------------------------------------------------------
  15. // Helpers
  16. // ------------------------------------------------------------------------------
  17. /**
  18. * Remove the given value from the array.
  19. * @template T
  20. * @param {T[]} array - The array to remove.
  21. * @param {T} x - The item to be removed.
  22. * @returns {void}
  23. */
  24. function remove (array, x) {
  25. const index = array.indexOf(x)
  26. if (index !== -1) {
  27. array.splice(index, 1)
  28. }
  29. }
  30. const signals = {
  31. SIGABRT: 6,
  32. SIGALRM: 14,
  33. SIGBUS: 10,
  34. SIGCHLD: 20,
  35. SIGCONT: 19,
  36. SIGFPE: 8,
  37. SIGHUP: 1,
  38. SIGILL: 4,
  39. SIGINT: 2,
  40. SIGKILL: 9,
  41. SIGPIPE: 13,
  42. SIGQUIT: 3,
  43. SIGSEGV: 11,
  44. SIGSTOP: 17,
  45. SIGTERM: 15,
  46. SIGTRAP: 5,
  47. SIGTSTP: 18,
  48. SIGTTIN: 21,
  49. SIGTTOU: 22,
  50. SIGUSR1: 30,
  51. SIGUSR2: 31,
  52. }
  53. /**
  54. * Converts a signal name to a number.
  55. * @param {string} signal - the signal name to convert into a number
  56. * @returns {number} - the return code for the signal
  57. */
  58. function convert (signal) {
  59. return signals[signal] || 0
  60. }
  61. // ------------------------------------------------------------------------------
  62. // Public Interface
  63. // ------------------------------------------------------------------------------
  64. /**
  65. * Run npm-scripts of given names in parallel.
  66. *
  67. * If a npm-script exited with a non-zero code, this aborts other all npm-scripts.
  68. *
  69. * @param {string} tasks - A list of npm-script name to run in parallel.
  70. * @param {object} options - An option object.
  71. * @returns {Promise} A promise object which becomes fullfilled when all npm-scripts are completed.
  72. * @private
  73. */
  74. module.exports = function runTasks (tasks, options) {
  75. return new Promise((resolve, reject) => {
  76. if (tasks.length === 0) {
  77. resolve([])
  78. return
  79. }
  80. const results = tasks.map(task => ({ name: task, code: undefined }))
  81. const queue = tasks.map((task, index) => ({ name: task, index }))
  82. const promises = []
  83. let error = null
  84. let aborted = false
  85. /**
  86. * Done.
  87. * @returns {void}
  88. */
  89. function done () {
  90. if (error == null) {
  91. resolve(results)
  92. } else {
  93. reject(error)
  94. }
  95. }
  96. /**
  97. * Aborts all tasks.
  98. * @returns {void}
  99. */
  100. function abort () {
  101. if (aborted) {
  102. return
  103. }
  104. aborted = true
  105. if (promises.length === 0) {
  106. done()
  107. } else {
  108. for (const p of promises) {
  109. p.abort()
  110. }
  111. Promise.all(promises).then(done, reject)
  112. }
  113. }
  114. /**
  115. * Runs a next task.
  116. * @returns {void}
  117. */
  118. function next () {
  119. if (aborted) {
  120. return
  121. }
  122. if (queue.length === 0) {
  123. if (promises.length === 0) {
  124. done()
  125. }
  126. return
  127. }
  128. const originalOutputStream = options.stdout
  129. const optionsClone = Object.assign({}, options)
  130. const writer = new MemoryStream(null, {
  131. readable: false,
  132. })
  133. if (options.aggregateOutput) {
  134. optionsClone.stdout = writer
  135. }
  136. const task = queue.shift()
  137. const promise = runTask(task.name, optionsClone)
  138. promises.push(promise)
  139. promise.then(
  140. (result) => {
  141. remove(promises, promise)
  142. if (aborted) {
  143. return
  144. }
  145. if (options.aggregateOutput) {
  146. originalOutputStream.write(writer.toString())
  147. }
  148. // Check if the task failed as a result of a signal, and
  149. // amend the exit code as a result.
  150. if (result.code === null && result.signal !== null) {
  151. // An exit caused by a signal must return a status code
  152. // of 128 plus the value of the signal code.
  153. // Ref: https://nodejs.org/api/process.html#process_exit_codes
  154. result.code = 128 + convert(result.signal)
  155. }
  156. // Save the result.
  157. results[task.index].code = result.code
  158. // Aborts all tasks if it's an error.
  159. if (result.code) {
  160. error = new NpmRunAllError(result, results)
  161. if (!options.continueOnError) {
  162. abort()
  163. return
  164. }
  165. }
  166. // Aborts all tasks if options.race is true.
  167. if (options.race && !result.code) {
  168. abort()
  169. return
  170. }
  171. // Call the next task.
  172. next()
  173. },
  174. (thisError) => {
  175. remove(promises, promise)
  176. if (!options.continueOnError || options.race) {
  177. error = thisError
  178. abort()
  179. return
  180. }
  181. next()
  182. }
  183. )
  184. }
  185. const max = options.maxParallel
  186. const end = (typeof max === 'number' && max > 0)
  187. ? Math.min(tasks.length, max)
  188. : tasks.length
  189. for (let i = 0; i < end; ++i) {
  190. next()
  191. }
  192. })
  193. }