Skip to content

readline with /dev/tty: echos and doesn't close #21319

Description

@coolaj86
  • Version: v10.2.1
  • Platform: Linux and macOS
  • Subsystem: readline

The use case is that a node script is being run from a piped bash script and so process.stdin is the pipe and /dev/tty must be used explicitly to get user input.

var readline = require('readline');
var input = require('fs').createReadStream('/dev/tty');
var rl = readline.createInterface({
  input: input
, output: process.stdout
//, terminal: false
});
rl.question("This is the prompt: ", function (response) {
  console.log("This is the response:", response);
  rl.close();
  // If I don't close the input explicitly, it stays open.
  input.close();
});

Output:

This is the prompt: yay
yay
This is the response: yay

Uncommenting terminal: false solves the issue of echoing, but creates a new problem in that it no longer treats stdout as a proper terminal.

Past issues that appear to be similar: #7965 nodejs/node-v0.x-archive#7101 https://stackoverflow.com/questions/24661774/createinterface-prints-double-in-terminal

Activity

  1. gireeshpunathil commented on Jun 14, 2018

    @gireeshpunathil
    Member
    (lldb) bt
    * thread #10
      * frame #0: 0x00007fff62f2214a libsystem_kernel.dylib`read + 10
        frame #1: 0x000000010092f2b6 node`uv__fs_buf_iter + 78
        frame #2: 0x000000010092cf29 node`uv__fs_work + 358
        frame #3: 0x0000000100929755 node`worker + 95
        frame #4: 0x00007fff630e8661 libsystem_pthread.dylib`_pthread_body + 340
        frame #5: 0x00007fff630e850d libsystem_pthread.dylib`_pthread_start + 377
        frame #6: 0x00007fff630e7bf9 libsystem_pthread.dylib`thread_start + 13
    (lldb) thr sel 1
    * thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
        frame #0: 0x00007fff62f21bf2 libsystem_kernel.dylib`kevent + 10
    libsystem_kernel.dylib`kevent:
    ->  0x7fff62f21bf2 <+10>: jae    0x7fff62f21bfc            ; <+20>
        0x7fff62f21bf4 <+12>: movq   %rax, %rdi
        0x7fff62f21bf7 <+15>: jmp    0x7fff62f17b00            ; cerror_nocancel
        0x7fff62f21bfc <+20>: retq   
    (lldb) bt
    * thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
      * frame #0: 0x00007fff62f21bf2 libsystem_kernel.dylib`kevent + 10
        frame #1: 0x000000010093abbb node`uv__io_poll + 836
        frame #2: 0x000000010092b88d node`uv_run + 315
        frame #3: 0x000000010003415c node`node::Start(v8::Isolate*, node::IsolateData*, int, char const* const*, int, char const* const*) + 780
        frame #4: 0x0000000100033c23 node`node::Start(uv_loop_s*, int, char const* const*, int, char const* const*) + 442
        frame #5: 0x00000001000333ba node`node::Start(int, char**) + 390
        frame #6: 0x0000000100001034 node`start + 52
    (lldb) 

    Basically the reader thread is engaged in reading from the device, and is blocked, immune to the stream close. This is closely related to #15439 and a documentation PR detailing the limitation is in progress through #21212

    input.close seem to be working in the code you provided but I don't know why it should, as the below code hangs too.

    var input = require('fs').createReadStream('/dev/tty');
    setTimeout(() => {
    input.close()
    }, 1000)

    pushing a null character into the device stream helps to unblock the reader thread.

    @@ -8,6 +8,7 @@ var rl = readline.createInterface({
     rl.question("This is the prompt: ", function (response) {
       console.log("This is the response:", response);
       rl.close();
    +  input.push(null);
       // If I don't close the input explicitly, it stays open.
       // input.close();
     });

    Hope this helps.

  2. coolaj86 commented on Jun 14, 2018

    @coolaj86
    Author

    Thanks.

    To your point, I found that the order in which readline and the input are closed seems to matter:

    rl.question("This is the prompt: ", function (response) {
      console.log("This is the response:", response);
      // If I close the input first, it still stays open.
      input.close();
      rl.close();
    });

    It may just be a goldilocks timing issue - or maybe the use of terminal: false changes it too. I haven't tried every permutation.

    I'll try your .push(null) and see how that works for me.

  3. addaleax commented on Jun 14, 2018

    @addaleax
    Member

    @coolaj86 Aside: You would probably rather want to use a TTY stream rather than an fs stream for accessing the TTY? That way you don’t need to worry about blocking reads

  4. coolaj86 commented on Jun 14, 2018

    @coolaj86
    Author

    @addaleax I'm not quite sure how that works. The docs talk a lot about process.stdin and process.stdout, but they don't actually give any examples of how to use tty.

    Based on playing around in the repl I'm guessing I might use it by doing something like this?

    var fd = fs.openSync('/dev/tty', 'r');
    var input = tty.ReadStream(fd);

    Could I somehow use tty on Windows too?

  5. addaleax commented on Jun 14, 2018

    @addaleax
    Member

    @coolaj86 Yes, exactly. :)

    Could I somehow use tty on Windows too?

    I don’t know about that. @nodejs/platform-windows

  6. bzoz commented on Jun 14, 2018

    @bzoz
    Contributor

    I guess fs.openSync('conin$', 'r') would be something similar on Windows, but it fails with ENOENT

  7. Fishrock123 commented on Jun 15, 2018

    @Fishrock123
    Contributor
    var fd = fs.openSync('/dev/tty', 'r');
    var input = tty.ReadStream(fd);

    This code seems correct to me, did that help @coolaj86?

    Could I somehow use tty on Windows too?

    So long as you know the stdin handle, I think so.

  8. added
    readlineIssues and PRs related to the built-in readline module.
    ttyIssues and PRs related to the tty subsystem.
    on Jun 15, 2018
  9. coolaj86 commented on Jun 24, 2018

    @coolaj86
    Author

    @Fishrock123 Yes, that is the correct way to do it on Mac and Linux.

    @gireeshpunathil I've tried a few different ways but can't tell if input.push(null) has any affect.

    It seems like node is now properly exiting, but leaving /dev/tty in a bad state. The shell will just hang until I hit enter and then the bash script that was calling the node script continues.

    For now I just print "Press any key to continue..." before calling process.exit(0) and then once enter is hit everything goes as expected.

  10. bnoordhuis commented on Jun 24, 2018

    @bnoordhuis
    Member

    It seems like node is now properly exiting, but leaving /dev/tty in a bad state. The shell will just hang until I hit enter and then the bash script that was calling the node script continues.

    You didn't mention how exactly you (or bash) start node, but if it's running in the background, then opening /dev/tty without the fs.constants.O_NOCTTY flag turns it into a controlling terminal. That's unlikely to be what you want.

  11. coolaj86 commented on Jun 28, 2018

    @coolaj86
    Author

    I added fs.constants.O_NOCTTY and it did not change the behavior.

    For context

    I made a convenient curl | bash installer to bootstrap the install process of my app:

    curl -fsSL https://get.example.app | bash

    When that script runs it installs an exact version of node (v10.2.1 presently as other versions we tested had particular bugs that broke the app) to a special prefix directory and (unzip / tar / gunzip)s the an exact version (tag) of the app from a git repo also into that same directory.

    installer.sh:

    #!/bin/bash
    
    # do some stuff to install node
    # do some stuff to unzip / untar our app
    # generate a bash file to call the app
    
    /path/to/app/bin/app --init --some options
    /path/to/app/bin/app

    The script generates and then calls another script which calls that version of node and the app, passing any options.

    /path/to/bin/app:

    #!/bin/bash
    /path/to/app/bin/node /path/to/app/bin/app.js "$@"

    Where it gets stuck

    # This is where it needs some additional input from the user to finish the install
    /path/to/app/bin/app --init --some options
    
    # That hangs and it doesn't progress to here:
    /path/to/app/bin/app
    

    We're now talking about 2 separate but related issues. I'll try to come up with a test case to show this second issue as well.

  12. Trott commented on Mar 15, 2021

    @Trott
    Member

    Hi, @coolaj86. I know this issue has been quiet for a very long time and it's possible you're not working on the relevant code base anymore. Which of the issues described here are you still experiencing (if any)?

  13. github-actions commented on Jun 27, 2026

    @github-actions
    Contributor

    This issue has been marked as stale due to 210 days of inactivity.
    It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.

  14. added
    staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.
    on Jun 27, 2026
  15. github-actions commented on Jul 28, 2026

    @github-actions
    Contributor

    This issue has been automatically closed after 30 days of inactivity following its stale status (no activity for a total of 120 days).
    If this is still relevant, feel free to reopen it or leave a comment with additional details so we can continue the discussion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    readlineIssues and PRs related to the built-in readline module.staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.ttyIssues and PRs related to the tty subsystem.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions