-
-
Notifications
You must be signed in to change notification settings - Fork 98
abort all processes when one fails #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,57 +1,125 @@ | ||
| import spawn from 'spawn-command' | ||
| import async from 'async' | ||
| import spawn from 'spawn-command-with-kill' | ||
| import Promise from 'bluebird' | ||
| import colors from 'colors/safe' | ||
| import {isString, find, clone} from 'lodash' | ||
| import {isString, clone} from 'lodash' | ||
| import {sync as findUpSync} from 'find-up' | ||
| import managePath from 'manage-path' | ||
| import arrify from 'arrify' | ||
| import getScriptToRun from './get-script-to-run' | ||
| import getScriptsFromConfig from './get-scripts-from-config' | ||
| import getLogger from './get-logger' | ||
|
|
||
| const noop = () => {} // eslint-disable-line func-style, no-empty-function | ||
| const NON_ERROR = 0 | ||
|
|
||
| export default runPackageScripts | ||
|
|
||
| function runPackageScripts({scriptConfig, scripts, args, options = {}}, callback = noop) { | ||
| function runPackageScripts({scriptConfig, scripts, args, options = {}}) { | ||
| if (scripts.length === 0) { | ||
| scripts = ['default'] | ||
| } | ||
| const scriptNames = arrify(scripts) | ||
| const method = options.parallel ? 'map' : 'mapSeries' | ||
| async[method](scriptNames, (scriptName, cb) => { | ||
| const child = runPackageScript({scriptConfig, options, scriptName, args}) | ||
| if (child.on) { | ||
| child.on('exit', exitCode => cb(null, exitCode)) | ||
| } else { | ||
| cb(child) | ||
| } | ||
| }, (err, results) => { | ||
| if (err) { | ||
| callback({error: err}) | ||
| } else { | ||
| const NON_ERROR = 0 | ||
| const result = find(results, r => r !== NON_ERROR) | ||
| callback({code: result}) | ||
| if (options.parallel) { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like the code below could be greatly simplified with RxJS. But I have no idea how to use Observables. Someone please help me! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really see an opportunity for observables. Might be missing something though. Why do you think observables make sense here?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've never used Observables, but I thought that they're supposed to really help with complex async stuff. 🤷 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it can be done a little bit cleaner though. I'll do a PR later today or this weekend. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know what I'm looking for.. I just think this code could be cleaned up a bit... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When running in parallel, you can just use const result = promises.reduce((res, task) => {
return res.then(() => task());
}, Promise.resolve());
result.then(...); |
||
| return runParallel() | ||
| } else { | ||
| return runSeries() | ||
| } | ||
|
|
||
| function runSeries() { | ||
| return scriptNames.reduce((res, scriptName) => { | ||
| return res.then(() => ( | ||
| runPackageScript({scriptConfig, options, scriptName, args}) | ||
| )) | ||
| }, Promise.resolve()) | ||
| } | ||
|
|
||
| function runParallel() { | ||
| const results = scriptNames.map(script => ({script, code: undefined})) | ||
| let aborted = false | ||
|
|
||
| const promises = scriptNames.map(scriptName => { | ||
| return runPackageScript({scriptConfig, options, scriptName, args}) | ||
| }) | ||
|
|
||
| const allPromise = Promise.all(promises.map((promise, index) => { | ||
| return promise.then(code => { | ||
| if (!aborted) { | ||
| results[index].code = code | ||
| } | ||
| }) | ||
| })).then(() => results) | ||
|
|
||
| allPromise.catch(() => { | ||
| /* istanbul ignore if */ | ||
| if (aborted) { | ||
| // this is very unlikely to happen | ||
| } else { | ||
| abortAll() | ||
| } | ||
| }) | ||
|
|
||
| return allPromise | ||
|
|
||
| function abortAll() { | ||
| aborted = true | ||
| promises.forEach(p => p.abort()) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| function runPackageScript({scriptConfig, options, scriptName, args}) { | ||
| const scripts = getScriptsFromConfig(scriptConfig, scriptName) | ||
| const script = getScriptToRun(scripts, scriptName) | ||
| if (!isString(script)) { | ||
| return { | ||
| return Promise.reject({ | ||
| message: colors.red( | ||
| `Scripts must resolve to strings. There is no script that can be resolved from "${scriptName}"` | ||
| ), | ||
| ref: 'missing-script', | ||
| } | ||
| }) | ||
| } | ||
| const command = [script, args].join(' ').trim() | ||
| const log = getLogger(getLogLevel(options)) | ||
| log.info(colors.gray('p-s executing: ') + colors.green(command)) | ||
| return spawn(command, {stdio: 'inherit', env: getEnv()}) | ||
| let child | ||
| const promise = new Promise((resolve, reject) => { | ||
| child = spawn(command, {stdio: 'inherit', env: getEnv()}) | ||
|
|
||
| child.on('error', error => { | ||
| child = null | ||
| reject({ | ||
| message: colors.red( | ||
| `The script called "${scriptName}" which runs "${command}" emitted an error` | ||
| ), | ||
| ref: 'emitted-an-error', | ||
| error, | ||
| }) | ||
| }) | ||
|
|
||
| child.on('close', code => { | ||
| child = null | ||
| if (code === NON_ERROR) { | ||
| resolve(code) | ||
| } else { | ||
| reject({ | ||
| message: colors.red( | ||
| `The script called "${scriptName}" which runs "${command}" failed with exit code ${code}` | ||
| ), | ||
| ref: 'failed-with-exit-code', | ||
| code, | ||
| }) | ||
| } | ||
| }) | ||
| }) | ||
|
|
||
| promise.abort = function abort() { | ||
| if (child !== null) { | ||
| child.kill() | ||
| child = null | ||
| } | ||
| } | ||
|
|
||
| return promise | ||
| } | ||
|
|
||
| function getLogLevel({silent, logLevel}) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Full lodash is quite huge, why not install them separately?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get you, but honestly I'm using about a dozen methods and I'd rather avoid installing each separately/managing them. It's 1.4MB (uncompressed) and the likelihood that someone's already depending on lodash in their project (even if it's transitively) is quite high.
Thanks for the suggestion.