-
-
Notifications
You must be signed in to change notification settings - Fork 222
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
Make the spawned process cancelable #189
Make the spawned process cancelable #189
Conversation
You need to add tests and docs too. |
There is CancelError being thrown. In test.js and readme, I've passed an empty function |
No, you're missing the point of CancelError. Please read the |
I think it would be nice to add properties similar to how
|
@ehmicky, I can't figure out how to do this. On cancelling p-cancelable itself throws CancelError and this is not notified in the handlePromise function - I mean in the if block which checks whether there is an error and and creates a proper execa error using makeError. |
Something like this might work if I understand your problem correctly: const processDone = new PCancelable((resolve, reject, onCancel) => {
let canceled = false;
spawned.on('exit', (code, signal) => {
cleanup();
resolve({code, signal, canceled});
});
/* more code */
onCancel(() => {
canceled = true;
spawned.kill();
});
}); |
This might work if you put the let canceled = false;
const processDone = new PCancelable((resolve, reject, onCancel) => {
/* more code */
onCancel(() => {
canceled = true;
spawned.kill();
});
});
const handlePromise = () => pFinally(Promise.all([
processDone,
getStream(spawned, 'stdout', {encoding, buffer, maxBuffer}),
getStream(spawned, 'stderr', {encoding, buffer, maxBuffer})
]).then(results => { // eslint-disable-line promise/prefer-await-to-then
const result = results[0];
result.stdout = results[1];
result.stderr = results[2];
if (result.error || result.code !== 0 || result.signal !== null || canceled) {
const error = makeError(result, {
joinedCommand,
parsed,
timedOut,
canceled
});
// TODO: missing some timeout logic for killed
// https://github.com/nodejs/node/blob/master/lib/child_process.js#L203
// error.killed = spawned.killed || killed;
error.killed = error.killed || spawned.killed;
if (!parsed.options.reject) {
return error;
}
throw error;
}
return {
stdout: handleOutput(parsed.options, result.stdout),
stderr: handleOutput(parsed.options, result.stderr),
code: 0,
failed: false,
killed: false,
signal: null,
cmd: joinedCommand,
timedOut: false,
canceled: false,
};
}), destroy); Notice the |
Ok I just realized from my above comment that the However when we cancel the promise, the child process is killed. This will fire So I suggest two possible solutions. First using Second not using const processDone = new Promise((resolve, reject) => {
spawned.on('exit', (code, signal) => {
cleanup();
resolve({code, signal});
});
spawned.on('error', error => {
cleanup();
resolve({error});
});
if (spawned.stdin) {
spawned.stdin.on('error', error => {
cleanup();
resolve({error});
});
}
});
let canceled = false;
processDone.cancel = function() {
if (canceled) { return; }
canceled = true;
spawned.kill();
} |
don't use ls add another test
@ehmicky I can't understand why is this happening but if I use
|
Could you post the code of that test? From intuition it seems like you should be doing |
I think using p-cancelable and allowing CancelError to be thrown is the best think which can be done; probably @sindresorhus asked to use p-cancelable because of that and why add a canceled property in result when the person using this can just check for CancelError(i mean use catch) 🤔 |
You're showing a different test that the initial failure. The initial failure you mentioned was about What you are trying to test also depends on how you will implement this feature. If there is a timing issue there, the issue is probably not the test but the implementation. Calling About letting the
Either of the two solutions above would solve those issues (or any third solution you might think of). |
sorry for troubling you so much over this PR. |
You are helping us add a new feature, there's nothing to be sorry about! Code review is part of the process and I enjoy it, so no worries. Hopefully this is a good experience for you as well, in case you wanted to contribute again after this PR :) I think the PR looks good now! Only two things missing:
|
@sindresorhus the PR looks good to me, but awaiting on your own code review before merging. |
index.js
Outdated
@@ -334,7 +341,8 @@ module.exports = (command, args, options) => { | |||
killed: false, | |||
signal: null, | |||
cmd: joinedCommand, | |||
timedOut: false | |||
timedOut: false, | |||
canceled: false |
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 would prefer to name it isCanceled
. I realize it's inconsistent with killed
, but I want to rename that one at some point too. Use the isCanceled
naming for all the canceled
variables.
index.js
Outdated
@@ -347,6 +355,15 @@ module.exports = (command, args, options) => { | |||
// eslint-disable-next-line promise/prefer-await-to-then | |||
spawned.then = (onFulfilled, onRejected) => handlePromise().then(onFulfilled, onRejected); | |||
spawned.catch = onRejected => handlePromise().catch(onRejected); | |||
spawned.cancel = function () { |
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.
Use arrow function
readme.md
Outdated
@@ -52,6 +52,15 @@ const execa = require('execa'); | |||
const {stdout} = await execa.shell('echo unicorns'); | |||
//=> 'unicorns' | |||
|
|||
// Cancelling a spawned process | |||
const spawned = execa("node"); |
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.
Single-quotes
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 think process
would be a better name than spawned
.
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.
Would this confuse readers about the global variable named process
? Child processes returned by spawn()
and global process
have different methods/properties (although some are shared).
Node API doc calls it subprocess
: https://nodejs.org/api/child_process.html#child_process_options_detached
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.
Ah yeah, subprocess
is better.
readme.md
Outdated
@@ -146,6 +155,13 @@ Execute a command synchronously through the system shell. | |||
|
|||
Returns the same result object as [`child_process.spawnSync`](https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options). | |||
|
|||
### spawned.cancel() |
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.
It's not clear enough what spawned
is. And it's weird to place the docs here. I think it would be better to document it in the execa()
section.
}); | ||
|
||
test('calling cancel method throws an error with message "Command was canceled"', async t => { | ||
const spawned = execa('noop'); |
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.
const spawned = execa('noop'); | |
const subprocess = execa('noop'); |
rename process to subprocess rename canceled in readme
index.js
Outdated
@@ -152,7 +152,7 @@ function makeError(result, options) { | |||
error = new Error(message); | |||
} | |||
|
|||
const prefix = getErrorPrefix({timedOut, timeout, signal, exitCodeName, exitCode}); | |||
const prefix = getErrorPrefix({timedOut, timeout, signal, exitCode, exitCodeName, isCanceled}); |
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.
Don't do unrelated changes
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 might have done this while fixing some merge conflict; that's the only time I touched those two variables exitCode
and exitCodeName
Thank you for contributing, @ammarbinfaisal :) |
see if this is okay.
I was wondering whether there should we a check if oncancel property in options object passed by someone is actually a function. is this required?
Fixes #113