Repository: sindresorhus/execa (JavaScript)

Issue:
EventEmitter memory leak
Steps to reproduce: 

`index.js`:

```js
import execa from 'execa'

(async () => {
  const childProcess = execa('echo', ['test'])
  await childProcess
  await childProcess
  await childProcess
  await childProcess
  await childProcess
})()
```

```bash
$ node index.js
(node:12764) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 close listeners added to [Socket]. Use emitter.setMaxListeners() to increase limit
(node:12764) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 close listeners added to [Socket]. Use emitter.setMaxListeners() to increase limit
```

Node `v12.4.0`
Ubuntu `19.10`
Execa `2.0.0`

This might not be an issue, but simply us setting more than 10 global listeners. But I would like to investigate this to make sure.
`result.all` is not constant
Steps to reproduce:

```js
import { promisify } from 'util'
import execa from 'execa'

(async () => {
  const childProcess = execa('echo', ['test'])
  await promisify(setTimeout)(1e3)

  console.log('all:', (await childProcess).all);
  console.log('all:', (await childProcess).all);
})()
```

```bash
$ node index.js
all: test
all: 
```

Node `v12.4.0`
Ubuntu `19.10`
Execa `2.0.0`


Discussion:
This seems to be a bug with [`pump`](https://github.com/mafintosh/pump), a dependency of [`get-stream`](https://github.com/sindresorhus/get-stream) that we use to retrieve streams with promises. [`Stream.pipeline()`](https://nodejs.org/api/stream.html#stream_stream_pipeline_streams_callback) (available since Node `10.0.0`) is a copy/paste of `pump` so I submitted a bug report [directly to Node](https://github.com/nodejs/node/issues/28432).

If this happened to a bug, I will submit a bug report to `pump` so they hopefully fix it (but the repository does not look actively maintained).
PR at #330.

I managed to reproduce this bug with this more minimal setup. It's based on what `execa` is internally doing.

```js
const childProcess = require('child_process');
const getStream = require('get-stream');
const mergeStream = require('merge-stream');

(async () => {
	const {stdout} = childProcess.spawn('echo', ['test']);

	const all = mergeStream();
	all.add(stdout);

	console.log('all:', await getStream(all));
	console.log('all:', await getStream(all));
})();
```

```bash
$ node index.js
all: test

all:
```
PR at #330.



Patch:
diff --git a/index.js b/index.js
index 3e3b125720..ef012681ff 100644
--- a/index.js
+++ b/index.js
@@ -4,6 +4,7 @@ const childProcess = require('child_process');
 const crossSpawn = require('cross-spawn');
 const stripFinalNewline = require('strip-final-newline');
 const npmRunPath = require('npm-run-path');
+const onetime = require('onetime');
 const makeError = require('./lib/error');
 const normalizeStdio = require('./lib/stdio');
 const {spawnedKill, spawnedCancel, setupTimeout, setExitHandler} = require('./lib/kill');
@@ -144,13 +145,15 @@ const execa = (file, args, options) => {
 		};
 	};
 
+	const handlePromiseOnce = onetime(handlePromise);
+
 	crossSpawn._enoent.hookChildProcess(spawned, parsed.parsed);
 
 	handleInput(spawned, parsed.options.input);
 
 	spawned.all = makeAllStream(spawned);
 
-	return mergePromise(spawned, handlePromise);
+	return mergePromise(spawned, handlePromiseOnce);
 };
 
 module.exports = execa;
diff --git a/package.json b/package.json
index 49c3db1410..57e69814c0 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
 		"is-stream": "^2.0.0",
 		"merge-stream": "^2.0.0",
 		"npm-run-path": "^3.0.0",
+		"onetime": "^5.1.0",
 		"p-finally": "^2.0.0",
 		"signal-exit": "^3.0.2",
 		"strip-final-newline": "^2.0.0"


Test patch:
diff --git a/test/test.js b/test/test.js
index 315419fdbb..4f0519a891 100644
--- a/test/test.js
+++ b/test/test.js
@@ -138,6 +138,12 @@ test('child_process.spawnSync() errors are propagated with a correct shape', t =
 	t.true(failed);
 });
 
+test('do not try to consume streams twice', async t => {
+	const cp = execa('noop', ['foo']);
+	t.is((await cp).stdout, 'foo');
+	t.is((await cp).stdout, 'foo');
+});
+
 test('use relative path with \'..\' chars', async t => {
 	const pathViaParentDir = path.join('..', path.basename(path.dirname(__dirname)), 'test', 'fixtures', 'noop');
 	const {stdout} = await execa(pathViaParentDir, ['foo']);