-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
child_process: allow promisified exec to be cancel #34249
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 1 commit
eed3f17
0ef841d
70cb86c
a6ae623
988ea3f
78e7521
c8210f6
3dca212
4163c44
9e3291e
3b14ebe
087f924
0fd0719
a41ac9a
f9af3fd
2b19cd5
f642015
dade71b
5e7aa7d
2305734
a60b935
fe077b0
72ff68a
026021a
c093035
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,12 +58,7 @@ let debug = require('internal/util/debuglog').debuglog( | |
| debug = fn; | ||
| } | ||
| ); | ||
| let DOMException; | ||
| function lazyDOMException(message) { | ||
| if (DOMException === undefined) | ||
| DOMException = internalBinding('messaging').DOMException; | ||
| return new DOMException(message); | ||
| } | ||
|
|
||
| const { Buffer } = require('buffer'); | ||
| const { Pipe, constants: PipeConstants } = internalBinding('pipe_wrap'); | ||
|
|
||
|
|
@@ -191,77 +186,28 @@ function exec(command, options, callback) { | |
| } | ||
|
|
||
| const customPromiseExecFunction = (orig) => { | ||
| return (file, options = {}) => { | ||
| return (...file) => { | ||
| let child; | ||
|
|
||
|
aduh95 marked this conversation as resolved.
Outdated
|
||
| if (typeof options !== 'object' || options === null) { | ||
| return PromiseReject( | ||
| new ERR_INVALID_ARG_TYPE( | ||
| 'options', | ||
| 'Object', | ||
| options)); | ||
| } | ||
|
|
||
| const { signal } = options; | ||
| const hasSignal = signal != null; | ||
|
|
||
| // Lets check if an AbortSignal is passed | ||
| if (hasSignal) { | ||
| const isNotValidSignal = | ||
| typeof signal !== 'object' || !('aborted' in signal); | ||
|
|
||
| if (isNotValidSignal) return PromiseReject( | ||
| new ERR_INVALID_ARG_TYPE( | ||
| 'options.signal', | ||
| 'AbortSignal', | ||
| signal)); | ||
|
|
||
| // Already aborted | ||
| if (signal.aborted) return PromiseReject(lazyDOMException('AbortError')); | ||
| // Check if a callback is passed somehow | ||
| if (typeof file[file.length - 1] === 'function') { | ||
| file.pop(); | ||
| } | ||
|
|
||
| const promise = new Promise((resolve, reject) => { | ||
| if (hasSignal) { | ||
| signal.addEventListener( | ||
| 'abort', | ||
| rejectOnAbortSignal(reject), | ||
| { once: true } | ||
| ); | ||
| } | ||
|
|
||
| child = orig(file, options, (err, stdout, stderr) => { | ||
| if (hasSignal && signal.aborted) { | ||
| reject(err); | ||
| } else if (err !== null) { | ||
| child = orig(...file, (err, stdout, stderr) => { | ||
| if (err !== null) { | ||
| err.stdout = stdout; | ||
| err.stderr = stderr; | ||
| reject(err); | ||
| } else { | ||
| resolve({ stdout, stderr }); | ||
| } | ||
|
|
||
| // Let's cleanup once exec is finished | ||
| if (hasSignal) | ||
| signal | ||
| .removeEventListener( | ||
| 'abort', | ||
| rejectOnAbortSignal(reject), | ||
| { once: true } | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| promise.child = child; | ||
|
|
||
| function rejectOnAbortSignal(reject) { | ||
| return () => { | ||
| if (promise && promise.child) | ||
| promise.child.kill(options.killSignal || 'SIGTERM'); | ||
|
|
||
| reject(lazyDOMException('AbortError')); | ||
| }; | ||
| } | ||
|
|
||
| return promise; | ||
| }; | ||
| }; | ||
|
|
@@ -315,9 +261,6 @@ function execFile(file /* , args, options, callback */) { | |
| // Validate maxBuffer, if present. | ||
| validateMaxBuffer(options.maxBuffer); | ||
|
|
||
| // Validate signal, if present | ||
| validateAbortSignal(options.signal, 'options.signal'); | ||
|
|
||
| options.killSignal = sanitizeKillSignal(options.killSignal); | ||
|
|
||
| const child = spawn(file, args, { | ||
|
|
@@ -327,7 +270,8 @@ function execFile(file /* , args, options, callback */) { | |
| uid: options.uid, | ||
| shell: options.shell, | ||
| windowsHide: !!options.windowsHide, | ||
| windowsVerbatimArguments: !!options.windowsVerbatimArguments | ||
| windowsVerbatimArguments: !!options.windowsVerbatimArguments, | ||
| signal: options.signal | ||
|
Member
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 if you want spawn to work here you'd need to use spawnWithSignal and not spawn, but I don't think that'd work since
Member
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. Yeah, see the test failures below
Member
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. Sure, basically it's getting called twice. I made a change on 8678797
aduh95 marked this conversation as resolved.
Outdated
|
||
| }); | ||
|
|
||
| let encoding; | ||
|
|
@@ -429,28 +373,12 @@ function execFile(file /* , args, options, callback */) { | |
| } | ||
| } | ||
|
|
||
| function abortHandler() { | ||
| if (!ex) | ||
| ex = new AbortError(); | ||
| process.nextTick(() => kill()); | ||
| } | ||
|
|
||
| if (options.timeout > 0) { | ||
| timeoutId = setTimeout(function delayedKill() { | ||
| kill(); | ||
| timeoutId = null; | ||
| }, options.timeout); | ||
| } | ||
| if (options.signal) { | ||
| if (options.signal.aborted) { | ||
| process.nextTick(abortHandler); | ||
| } else { | ||
| const childController = new AbortController(); | ||
| options.signal.addEventListener('abort', abortHandler, | ||
| { signal: childController.signal }); | ||
| child.once('close', () => childController.abort()); | ||
| } | ||
| } | ||
|
|
||
| if (child.stdout) { | ||
| if (encoding) | ||
|
|
@@ -674,10 +602,43 @@ function normalizeSpawnArguments(file, args, options) { | |
|
|
||
| function spawn(file, args, options) { | ||
| const child = new ChildProcess(); | ||
|
|
||
| options = normalizeSpawnArguments(file, args, options); | ||
|
|
||
| if (options.signal) { | ||
| const signal = options.signal; | ||
| // Validate signal, if present | ||
| validateAbortSignal(signal, 'options.signal'); | ||
|
|
||
| const onAbortListener = onAbortSignal(child); | ||
|
aduh95 marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Do nothing and throw if already aborted | ||
| if (signal.aborted) { | ||
| onAbortListener(); | ||
| } else { | ||
| signal.addEventListener('abort', onAbortListener, { once: true }); | ||
| child.once('close', () => signal.removeEventListener('abort', onAbortListener, { once: true })); | ||
| } | ||
|
|
||
| function onAbortSignal(childProcess) { | ||
| return function abortListener() { | ||
| if (childProcess.stdout) childProcess.stdout.destroy(); | ||
|
|
||
| if (childProcess.stderr) childProcess.stderr.destroy(); | ||
|
|
||
| process.nextTick(() => { | ||
| if (childProcess && childProcess.kill) { | ||
| childProcess.kill(options.killSignal); | ||
| } | ||
|
|
||
| childProcess.emit('error', new AbortError()); | ||
| }); | ||
| } | ||
| } | ||
|
aduh95 marked this conversation as resolved.
|
||
| } | ||
|
|
||
| debug('spawn', options); | ||
| child.spawn(options); | ||
|
|
||
|
|
||
| return child; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| 'use strict'; | ||
|
|
||
| const common = require('../common'); | ||
| const assert = require('assert'); | ||
| const { promisify } = require('util'); | ||
| const execFile = require('child_process').execFile; | ||
| const { getSystemErrorName } = require('util'); | ||
| const fixtures = require('../common/fixtures'); | ||
|
|
||
| const fixture = fixtures.path('exit.js'); | ||
| const echoFixture = fixtures.path('echo.js'); | ||
| const execOpts = { encoding: 'utf8', shell: true }; | ||
| const execFilePromisified = promisify(execFile); | ||
|
|
||
| { | ||
| // Verify that the signal option works properly | ||
| const ac = new AbortController(); | ||
| const signal = ac.signal; | ||
| const promise = execFilePromisified(process.execPath, [echoFixture, 0], { signal }); | ||
|
|
||
| ac.abort(); | ||
|
|
||
| assert.rejects(promise, /AbortError/); | ||
| } | ||
|
|
||
| { | ||
| // Verify that the signal option works properly when already aborted | ||
| const ac = new AbortController(); | ||
| const { signal } = ac; | ||
| ac.abort(); | ||
|
|
||
| const promise = execFilePromisified(process.execPath, [echoFixture, 0], { signal }); | ||
|
|
||
| assert.rejects(promise, /AbortError/); | ||
| } | ||
|
|
||
| { | ||
| // Verify that if something different than Abortcontroller.signal | ||
| // is passed, ERR_INVALID_ARG_TYPE is thrown | ||
| const promise = execFilePromisified(process.execPath, [echoFixture, 0], { signal: {} }); | ||
|
|
||
| assert.rejects(promise, { | ||
| code: 'ERR_INVALID_ARG_TYPE', | ||
| name: 'TypeError', | ||
| message: 'The "options.signal" property must be an ' + | ||
| 'instance of AbortSignal. Received an instance of Object' | ||
| }); | ||
|
|
||
| } | ||
|
|
||
| { | ||
| // Verify that if something different than Abortcontroller.signal | ||
| // is passed, ERR_INVALID_ARG_TYPE is thrown | ||
| const promise = execFilePromisified(process.execPath, [echoFixture, 0], { signal: 'world!' }); | ||
|
|
||
| assert.rejects(promise, { | ||
| code: 'ERR_INVALID_ARG_TYPE', | ||
| name: 'TypeError', | ||
| message: 'The "options.signal" property must be an ' + | ||
| `instance of AbortSignal. Received type string ('world!')` | ||
| }); | ||
|
|
||
| } |
Uh oh!
There was an error while loading. Please reload this page.