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

Piping process.stdin to child.stdin leaves behind an open handle #415

Closed
jsumners opened this issue Feb 26, 2020 · 10 comments
Closed

Piping process.stdin to child.stdin leaves behind an open handle #415

jsumners opened this issue Feb 26, 2020 · 10 comments
Labels

Comments

@jsumners
Copy link

'use strict'

const execa = require('execa')
const wtfnode = require('wtfnode')

main()
  .then(() => {
    console.log('!!! done')
    wtfnode.dump()
  })
  .catch(console.error)

async function main () {
  console.log('write something and press "enter"')
  const child = execa('read', ['foo'])
  process.stdin.pipe(child.stdin)

  await child
  process.stdin.unpipe(child.stdin)
}

Run that script and watch the process hang because there is an open handle in the event loop. Thus far, my only solution is to add a process.stdin.unref().

@ehmicky
Copy link
Collaborator

ehmicky commented Feb 26, 2020

Hi @jsumners,

Thanks for reaching out.

This is intended behavior.
read waits for the next line of stdin before completing.
Therefore, if you run the above script in an interactive terminal, the read command will wait for the next newline inside process.stdin (which would be the user's terminal input). Execa will wait on the read command to complete before completing itself (await child).

If you run the above script in a terminal and type a line of input then "Enter", await child will be resolve.

@jsumners
Copy link
Author

Yes, that's my point. read foo completes because the user writes something and presses "enter". The read command finishes and then execa never releases.

@ehmicky
Copy link
Collaborator

ehmicky commented Feb 26, 2020

I could not reproduce this. I am using the latest version of Execa (4.0.0) on Linux.
When running node script.js then entering something, the script correctly exits.

@ehmicky ehmicky reopened this Feb 26, 2020
@ehmicky
Copy link
Collaborator

ehmicky commented Feb 26, 2020

I think I found the issue: read is a Bash built-in command. By default Execa commands do not run within a shell (such as Bash) due to the following reasons.

If you pass the shell: true option, this might solve your problem.

@jsumners
Copy link
Author

@jsumners
Copy link
Author

Here's the catch: I'm seeing this behavior when wrapping aws-okta (https://github.com/segmentio/aws-okta) and having to enter an MFA code. It's not a shell built-in but is showing the behavior replicated in my example.

@ehmicky
Copy link
Collaborator

ehmicky commented Feb 26, 2020

I believe read is in fact a shell built-in, i.e. there is no binary file for it, instead it works only within a shell. What happens when you use the shell: true option?

@jsumners
Copy link
Author

Can we get past read being a built-in and consider what I am actually trying to report?

Create a simple echo utility:

foo.c:

#include <stdio.h>
int main() {
  char str[100];
  gets(str);
  puts(str);
  return 0;
}

Compile it: gcc -o foo foo.c

Update the provided example:

index.js:

'use strict'

const execa = require('execa')
const wtfnode = require('wtfnode')

main()
  .then(() => {
    console.log('!!! done')
    wtfnode.dump()
  })
  .catch(console.error)

async function main () {
  console.log('write something and press "enter"')
  const child = execa('./foo')
  process.stdin.pipe(child.stdin)

  await child
  process.stdin.unpipe(child.stdin)
}

Run node index.js, type something, press "enter", and watch the script hang.

@jsumners
Copy link
Author

This version of the script works as expected:

'use strict'

const execa = require('execa')
const wtfnode = require('wtfnode')

main()
  .then(() => {
    console.log('!!! done')
    wtfnode.dump()
  })
  .catch(console.error)

async function main () {
  console.log('write something and press "enter"')
  await execa('./foo', [], { stdio: 'inherit' })
}

@ehmicky
Copy link
Collaborator

ehmicky commented Mar 15, 2020

This bug can be reduced to the following script:

const execa = require('execa')

const child = execa('./foo')
process.stdin.pipe(child.stdin)
child.then(() => {
  console.log('done')
  process.stdin.unpipe(child.stdin)
})

The entered line is correctly read on stdin. The unexpected behavior is that the main process does not exit until another newline is entered.

Now it turns out this seems to be an issue with Node.js itself, not with Execa. You can reproduce it with:

const { spawn } = require('child_process')

const child = spawn('./foo')
process.stdin.pipe(child.stdin)
child.on('exit', () => {
  console.log('done')
  process.stdin.unpipe(child.stdin)
})

I opened nodejs/node#32291 to track this bug in Node.js. I will close this issue since this is not caused by Execa itself.

@ehmicky ehmicky closed this as completed Mar 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants