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.
218 lines
5.2 KiB
218 lines
5.2 KiB
1 month ago
|
/**
|
||
|
* @module run-tasks-in-parallel
|
||
|
* @author Toru Nagashima
|
||
|
* @copyright 2015 Toru Nagashima. All rights reserved.
|
||
|
* See LICENSE file in root directory for full license.
|
||
|
*/
|
||
|
'use strict'
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// Requirements
|
||
|
// ------------------------------------------------------------------------------
|
||
|
|
||
|
const MemoryStream = require('memorystream')
|
||
|
const NpmRunAllError = require('./npm-run-all-error')
|
||
|
const runTask = require('./run-task')
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// Helpers
|
||
|
// ------------------------------------------------------------------------------
|
||
|
|
||
|
/**
|
||
|
* Remove the given value from the array.
|
||
|
* @template T
|
||
|
* @param {T[]} array - The array to remove.
|
||
|
* @param {T} x - The item to be removed.
|
||
|
* @returns {void}
|
||
|
*/
|
||
|
function remove (array, x) {
|
||
|
const index = array.indexOf(x)
|
||
|
if (index !== -1) {
|
||
|
array.splice(index, 1)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const signals = {
|
||
|
SIGABRT: 6,
|
||
|
SIGALRM: 14,
|
||
|
SIGBUS: 10,
|
||
|
SIGCHLD: 20,
|
||
|
SIGCONT: 19,
|
||
|
SIGFPE: 8,
|
||
|
SIGHUP: 1,
|
||
|
SIGILL: 4,
|
||
|
SIGINT: 2,
|
||
|
SIGKILL: 9,
|
||
|
SIGPIPE: 13,
|
||
|
SIGQUIT: 3,
|
||
|
SIGSEGV: 11,
|
||
|
SIGSTOP: 17,
|
||
|
SIGTERM: 15,
|
||
|
SIGTRAP: 5,
|
||
|
SIGTSTP: 18,
|
||
|
SIGTTIN: 21,
|
||
|
SIGTTOU: 22,
|
||
|
SIGUSR1: 30,
|
||
|
SIGUSR2: 31,
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Converts a signal name to a number.
|
||
|
* @param {string} signal - the signal name to convert into a number
|
||
|
* @returns {number} - the return code for the signal
|
||
|
*/
|
||
|
function convert (signal) {
|
||
|
return signals[signal] || 0
|
||
|
}
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// Public Interface
|
||
|
// ------------------------------------------------------------------------------
|
||
|
|
||
|
/**
|
||
|
* Run npm-scripts of given names in parallel.
|
||
|
*
|
||
|
* If a npm-script exited with a non-zero code, this aborts other all npm-scripts.
|
||
|
*
|
||
|
* @param {string} tasks - A list of npm-script name to run in parallel.
|
||
|
* @param {object} options - An option object.
|
||
|
* @returns {Promise} A promise object which becomes fullfilled when all npm-scripts are completed.
|
||
|
* @private
|
||
|
*/
|
||
|
module.exports = function runTasks (tasks, options) {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
if (tasks.length === 0) {
|
||
|
resolve([])
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const results = tasks.map(task => ({ name: task, code: undefined }))
|
||
|
const queue = tasks.map((task, index) => ({ name: task, index }))
|
||
|
const promises = []
|
||
|
let error = null
|
||
|
let aborted = false
|
||
|
|
||
|
/**
|
||
|
* Done.
|
||
|
* @returns {void}
|
||
|
*/
|
||
|
function done () {
|
||
|
if (error == null) {
|
||
|
resolve(results)
|
||
|
} else {
|
||
|
reject(error)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Aborts all tasks.
|
||
|
* @returns {void}
|
||
|
*/
|
||
|
function abort () {
|
||
|
if (aborted) {
|
||
|
return
|
||
|
}
|
||
|
aborted = true
|
||
|
|
||
|
if (promises.length === 0) {
|
||
|
done()
|
||
|
} else {
|
||
|
for (const p of promises) {
|
||
|
p.abort()
|
||
|
}
|
||
|
Promise.all(promises).then(done, reject)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Runs a next task.
|
||
|
* @returns {void}
|
||
|
*/
|
||
|
function next () {
|
||
|
if (aborted) {
|
||
|
return
|
||
|
}
|
||
|
if (queue.length === 0) {
|
||
|
if (promises.length === 0) {
|
||
|
done()
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const originalOutputStream = options.stdout
|
||
|
const optionsClone = Object.assign({}, options)
|
||
|
const writer = new MemoryStream(null, {
|
||
|
readable: false,
|
||
|
})
|
||
|
|
||
|
if (options.aggregateOutput) {
|
||
|
optionsClone.stdout = writer
|
||
|
}
|
||
|
|
||
|
const task = queue.shift()
|
||
|
const promise = runTask(task.name, optionsClone)
|
||
|
|
||
|
promises.push(promise)
|
||
|
promise.then(
|
||
|
(result) => {
|
||
|
remove(promises, promise)
|
||
|
if (aborted) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if (options.aggregateOutput) {
|
||
|
originalOutputStream.write(writer.toString())
|
||
|
}
|
||
|
|
||
|
// Check if the task failed as a result of a signal, and
|
||
|
// amend the exit code as a result.
|
||
|
if (result.code === null && result.signal !== null) {
|
||
|
// An exit caused by a signal must return a status code
|
||
|
// of 128 plus the value of the signal code.
|
||
|
// Ref: https://nodejs.org/api/process.html#process_exit_codes
|
||
|
result.code = 128 + convert(result.signal)
|
||
|
}
|
||
|
|
||
|
// Save the result.
|
||
|
results[task.index].code = result.code
|
||
|
|
||
|
// Aborts all tasks if it's an error.
|
||
|
if (result.code) {
|
||
|
error = new NpmRunAllError(result, results)
|
||
|
if (!options.continueOnError) {
|
||
|
abort()
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Aborts all tasks if options.race is true.
|
||
|
if (options.race && !result.code) {
|
||
|
abort()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Call the next task.
|
||
|
next()
|
||
|
},
|
||
|
(thisError) => {
|
||
|
remove(promises, promise)
|
||
|
if (!options.continueOnError || options.race) {
|
||
|
error = thisError
|
||
|
abort()
|
||
|
return
|
||
|
}
|
||
|
next()
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
|
||
|
const max = options.maxParallel
|
||
|
const end = (typeof max === 'number' && max > 0)
|
||
|
? Math.min(tasks.length, max)
|
||
|
: tasks.length
|
||
|
for (let i = 0; i < end; ++i) {
|
||
|
next()
|
||
|
}
|
||
|
})
|
||
|
}
|