The Wayback Machine - https://web.archive.org/web/20210103084148/https://github.com/sindresorhus/execa/pull/424
Skip to content
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

Duplex stream #424

Open
wants to merge 17 commits into
base: master
from
Open

Duplex stream #424

wants to merge 17 commits into from

Conversation

@sloonz
Copy link

@sloonz sloonz commented May 15, 2020

First shot at an implementation for fixes #143

failingStream.destroy(new Error('oops'));
const {message} = await t.throwsAsync(pipeline(failingStream, duplex, makeCollector()));
t.is(message, 'oops');
const err = await t.throwsAsync(duplex);

This comment has been minimized.

@sindresorhus

sindresorhus May 16, 2020
Owner

err => error

@sindresorhus
Copy link
Owner

@sindresorhus sindresorhus commented May 16, 2020

Thanks for working on this 🙌

Make sure you add a lot of tests.

@jschatz1

This comment has been minimized.

Copy link

@jschatz1 jschatz1 commented on index.js in 9708d7c May 17, 2020

@sloonz
Copy link
Author

@sloonz sloonz commented May 17, 2020

No need to implement writev, it's optional method that is here for optimization purposes if you can handle multiple chunks at once, which we can't since we just pass them to process stdin.

Do you have an idea on what other tests I could make ?

Unless someone can lend me a hand, it will take some times for me to track the test failures on windows ; I don't have access to a windows machine right now.

@sloonz
Copy link
Author

@sloonz sloonz commented May 21, 2020

Okay, found why some tests where failing on Windows and not Linux, but that raises more questions @sindresorhus

It’s on unit tests testing this code path : https://github.com/sloonz/execa/blob/28ac048159f6ea1fa2c27499451c014a74e80265/index.js#L268-L287

I tried to trigger an exception on line 266 by passing null to file. This works as intended on Linux ; however, on Windows, the exception gets triggered earlier (at line 261, handleArgs), and the exception directly unwind to the caller instead of being caught and handled.

Two questions :

  • How can I consistently trigger an exception on childProcess.spawn ?
  • I have not tested, but my problem most likely is also here for the main execa() function, where execa(null) returns an error (which doubles as a rejected promise) on Linux whereas it just throws on Windows. Is this the intended behavoir ? Should I fill a bug for this ?
@sloonz sloonz force-pushed the sloonz:duplex-stream branch 2 times, most recently from 6b273d5 to 7cbc2ad May 25, 2020
Simon Lipp
@sloonz sloonz force-pushed the sloonz:duplex-stream branch from 53be384 to fd24f91 May 26, 2020
@sloonz
Copy link
Author

@sloonz sloonz commented May 26, 2020

I think the feature is in a good spot for review/possible integration in the current iteration ; should I squash the commits ?

index.d.ts Outdated
@@ -544,6 +544,20 @@ declare const execa: {
): execa.ExecaChildProcess<Buffer>;
node(scriptPath: string, options?: execa.Options): execa.ExecaChildProcess;
node(scriptPath: string, options?: execa.Options<null>): execa.ExecaChildProcess<Buffer>;

/**
Execute a file as a transform stream

This comment has been minimized.

@sindresorhus

sindresorhus Jun 1, 2020
Owner

Suggested change
Execute a file as a transform stream
Execute a file as a transform stream.

And it's not a transform stream.

index.js Outdated
result.then(() => {}).catch(error => error),
new Promise((resolve, _) => stdin.on('close', resolve)),
new Promise((resolve, _) => output.on('close', resolve))
]).then(([error]) => {

This comment has been minimized.

@sindresorhus

sindresorhus Jun 1, 2020
Owner

Use await.

index.js Outdated
});

Promise.all([
result.then(() => {}).catch(error => error),

This comment has been minimized.

@sindresorhus

sindresorhus Jun 1, 2020
Owner

Do you really need .then here?

This comment has been minimized.

@sloonz

sloonz Jun 17, 2020
Author

Yes, otherwise non-error result will be resolved by the complete promise. A more explicit intent would be

result.then(() => { return null; }).catch(error => error)

But anyway this is changed by the use of await instead of then.

index.js Outdated
}
});

const result = processDone.then(({error, exitCode, signal, timedOut}) => {

This comment has been minimized.

@sindresorhus

sindresorhus Jun 1, 2020
Owner

Use await.


function makeCollector() {
const chunks = [];
const w = new stream.Writable({

This comment has been minimized.

@sindresorhus

sindresorhus Jun 1, 2020
Owner

Use descriptive variable names.

function makeCollector() {
const chunks = [];
const w = new stream.Writable({
write(chunk, encoding, cb) {

This comment has been minimized.

@sindresorhus

sindresorhus Jun 1, 2020
Owner

Suggested change
write(chunk, encoding, cb) {
write(chunk, encoding, callback) {
const duplex = execa.duplexStream('forever');
const failingStream = makeEmptyReadableStream();
failingStream.destroy(new Error('oops'));
const {message} = await t.throwsAsync(pipeline(failingStream, duplex, makeCollector()));

This comment has been minimized.

@sindresorhus

sindresorhus Jun 1, 2020
Owner

Suggested change
const {message} = await t.throwsAsync(pipeline(failingStream, duplex, makeCollector()));
await t.throwsAsync(pipeline(failingStream, duplex, makeCollector()), {message: 'oops'});
@sindresorhus
Copy link
Owner

@sindresorhus sindresorhus commented Jun 1, 2020

should I squash the commits ?

No need. We'll squash when merging.

@sindresorhus
Copy link
Owner

@sindresorhus sindresorhus commented Jun 1, 2020

I would like to see even more tests for this. It's a complicated feature (streams have a lot of edge-cases).

@sloonz
Copy link
Author

@sloonz sloonz commented Jun 17, 2020

Sorry for the hiatus.

New iteration of the branch is there.

No new test because my dumb brain can't generate any idea of what to test that isn't currently covered. Do you have any suggestion ?

@sloonz
Copy link
Author

@sloonz sloonz commented Jul 8, 2020

Small bump ?

@sindresorhus
Copy link
Owner

@sindresorhus sindresorhus commented Aug 10, 2020

Would be nice to DRY up some code between the duplex method and the others.

index.js Outdated
return error;
}
})(),
new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars

This comment has been minimized.

@sindresorhus

sindresorhus Aug 10, 2020
Owner

Don't disable the rule. Just adhere to it.

index.js Outdated
})();

(async () => {
const promisesResult = (await Promise.all([

This comment has been minimized.

@sindresorhus

sindresorhus Aug 10, 2020
Owner

Use destructuring.

index.js Outdated
(async () => {
try {
await result;
return null;

This comment has been minimized.

@sindresorhus

sindresorhus Aug 10, 2020
Owner

Suggested change
return null;
return;

use undefined

index.js Outdated
output.on('close', resolve);
})
]));
if (promisesResult[0] !== null) {

This comment has been minimized.

@sindresorhus

sindresorhus Aug 10, 2020
Owner

Suggested change
if (promisesResult[0] !== null) {
if (promisesResult[0] !== null) {
} catch (error) {
const errorPromise = Promise.reject(makeError({
error,
stdout: undefined,

This comment has been minimized.

@sindresorhus

sindresorhus Aug 10, 2020
Owner

This should not be undefined.

callback();
}
});
writableStream.collect = () => Buffer.concat(chunks).toString('utf-8');

This comment has been minimized.

@sindresorhus

sindresorhus Aug 10, 2020
Owner

Suggested change
writableStream.collect = () => Buffer.concat(chunks).toString('utf-8');
writableStream.collect = () => Buffer.concat(chunks).toString('utf8');
t.is(message, 'oops');
const error = await t.throwsAsync(duplex);
t.is(error.isCanceled, true);
t.is(error.killed, true);

This comment has been minimized.

@sindresorhus

sindresorhus Aug 10, 2020
Owner

Suggested change
t.is(error.killed, true);
t.true(error.killed);
@sindresorhus
Copy link
Owner

@sindresorhus sindresorhus commented Aug 10, 2020

In general for PRs:

  • Look over your own diff.
  • Try to improve the implementation.
  • Try to refactor and simplify.
  • Improve docs.
@sloonz
Copy link
Author

@sloonz sloonz commented Sep 23, 2020

I would like your opinion on something.

I planned to refactor in two parts, but now I wonder if the second part is needed.

In the previous implementation the result of duplexStream was both a duplex stream AND and execa object (spawned process object + promise). Now I wonder if this is a good idea, or if the result of duplexStream should not be just a stream and don't return the spawned process or a promise at all

Pros: way simpler code and interface
Cons: can't wait for the process to be killed after a failure in the pipeline (a kill signal is sent if the pipeline fails, but with just the duplex stream you have no way of waiting for the process to actually have ended) ; can't get the exit code (or really any information) of the process after a failure in the pipeline. I think the biggest con is that I see no way of unit testing "pipeline failure should kill the process".

I tend to prefer the simpler solution, but at the end it's your call :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
3 participants
You can’t perform that action at this time.