20

In the following code, I assign a listener to the data event of process.stdin with the once method.

console.log('Press Enter to allow process to terminate')
process.stdin.once('data', callback)

function callback (data) {
    console.log('Process can terminate now')
}

In theory, when the callback has fired, the event listener should be automatically removed (because I attached it with once), allowing the process to terminate. Surprisingly, in this case, the process never terminates (The code you see is the whole thing, try it!). I also tried manually removing the listener, but that changes nothing.

Is there something else going on here that I don't realise perhaps?

Shawn
  • 10,931
  • 18
  • 81
  • 126

2 Answers2

36

Adding the data event listener to process.stdin add a reference to it that keeps the process open. That reference stays in place even after removing all event listeners. What you can do is manually unref() it in your callback, like so:

console.log('Press Enter to allow process to terminate')
process.stdin.once('data', callback)

function callback (data) {
    console.log('Process can terminate now')
    process.stdin.unref()
}

Also, as a general debugging tool for stuff like this, there are two (undocumented) functions that you can call to get a list of things keeping your process open:

process._getActiveHandles()
process._getActiveRequests()

See this pull request in the node project for background.


Update: You asked about attaching event listeners after you've unref()'d process.stdin. Here's a quick example showing that the listener does attach itself and function:

console.log('Press Enter to allow process to terminate')
process.stdin.once('data', callback)

function callback (data) {
    console.log('Unreferencing stdin. Exiting in 5 seconds.')
    process.stdin.unref()

    process.stdin.once('data', function(data) {
        console.log('More data')
    })

    setTimeout(function() {
        console.log('Timeout, Exiting.')
    }, 5000);
}

With that code, if you press another key before the setTimeout fires (5 seconds), then you'll see More data output to the console. Once the setTimeout's callback fires, the process will exit. The trick is that setTimeout is creating a timer which the process also keeps a reference too. Since the process still has a reference to something, it won't exit right away. Once the timer fires, the reference it released and the process exits. This also shows that references are added (and removed) to things that need them automatically (the timer created by setTimeout in this case).

Mike S
  • 41,895
  • 11
  • 89
  • 84
  • Once I've remove the reference, it seems I can no longer attach event listeners to stdin. Try `process.stdin.once('data', function (d) { console.log('logged'); this.unref(); process.stdin.once('data', function (d2) { console.log('never logged') }) })`. This logs "logged", then waits for me to press Enter. When I press Enter, the process terminates. – Shawn Sep 24 '14 at 08:59
  • 1
    Technically, the event listener is added, it just never fires. The reference to `process.stdin` is the only thing keeping the process open in this case. Once you manually remove it, the process will exit at the end of the current run loop so there's no chance for the event listener you added to ever fire. – Mike S Sep 24 '14 at 17:03
  • Why then does it keep the process open the first time around? – Shawn Sep 24 '14 at 17:21
  • 1
    Things that have references will automatically have one set when they are created, so the process starts off with a reference to `process.stdin` and that keeps it open. – Mike S Sep 24 '14 at 17:27
  • 1
    I updated my answer with an example showing that event listeners added after `unref()` still work (as long as something is keeping the process open). – Mike S Sep 24 '14 at 17:38
  • Very nice. Now how could I achieve the same without a timer? I know I can unref only in the second callback, but in more complex code, I may not want to keep track of which the last one is. – Shawn Sep 24 '14 at 17:41
  • 1
    Well, you can only `unref()` `process.stdin`, you can't `ref()` it again. So, you have to be sure that you're done with it when you call `unref()`. The timer trick was just that, a trick; I wouldn't use something like that in most cases. If you have a bunch of things happening simultaneously (asynchronously) and you need to wait for all of them to finish, then you'll probably want to look in to something like the [async module](https://github.com/caolan/async), or promises. With either of those you can get a callback when all your async tasks are complete; in there you can call `unref()`. – Mike S Sep 24 '14 at 17:49
  • What the hell is going on with this answer? It's using undocumented APIs and it's just plain weird when the alternative is a very straightforward and documented solution. @Shawn there's clearly some fundamental knowledge you're lacking if you're trying to attach event listeners to completed (ended) streams. – Mulan Sep 28 '14 at 19:32
  • 1
    @naomik Well, I'm not attaching listeners to completed streams anymore :p Indeed, this answer provided me with the knowledge I was lacking. I actually use `end()` in my code, not `unref()`. The reason I chose this answer is that it explains what's going in addition to saying how to solve the problem. In both cases, the solution works, but in this case, the answer gives me the knowledge to understand why it won't work twice. Make sense? – Shawn Sep 29 '14 at 08:04
  • 1
    I have been wanting to find something like `process._getActiveHandles()` and `process._getActiveRequests()` for years and somehow never came across them. Thanks! – Michael Lang Oct 09 '15 at 02:58
  • 1
    ```end()``` is not a documented solution. ```process.stdin``` is documented as a ```Readable Stream```. ```end()``` is a method on ```Writable Stream```. – Greg May 20 '16 at 00:51
  • It took me a long time to understand my node process was not finishing because there were event listeners attached to process.stdin, and it was very confusing when I removed them but the problem persisted. Using unref() as mentioned above did the trick. Now I've read the documentation about process.stdin and also learned about the newer paused mode, versus the legacy flowing mode. I found I don't need to use the undocumented unref() if I remove all the listeners and then call process.stdin.pause(). – WeakPointer Feb 06 '17 at 22:11
10

Just call .end on the process.stdin stream

To me, this is a more straightforward (and documented) way of ending the stream.

console.log('Press Enter to allow process to terminate');
process.stdin.once('data', callback);

function callback (data) {
  console.log('Process can terminate now');
  process.stdin.end();
}

It's also worth noting that node sets the stream as the context for the callback function, so you can just call this.end

console.log('Press Enter to allow process to terminate');
process.stdin.once('data', callback);

function callback (data) {
  // `this` refers to process.stdin here
  console.log('Process can terminate now');
  this.end();
}

You could also emit an end event which has additional benefits like being able to call a function when the stream is finished.

console.log('Press Enter to allow process to terminate');

process.stdin.once('data', function(data) {
  console.log('Process can terminate now');
  this.emit("end");
});

process.stdin.on('end', function() {
  console.log("all done now");
});

This would output

Press Enter to allow process to terminate

Process can terminate now
all done now

A final solution would be to use process.exit. This allows you to terminate a program whenver you want.

for (var i=0; i<10; i++) {
  process.stdout.write( i.toString() );
  if (i > 3) process.exit();
}

Output

01234

This would work inside of a stream callback, as part of a child process, or any other bit of code.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • So many options! But for others seeing this answer, do check Mike S's answer as well as it also explains why this is necessary, and gives yet another option. – Shawn Sep 24 '14 at 08:51
  • Once I've remove the reference, it seems I can no longer attach event listeners to stdin. Try process.stdin.once('data', function (d) { console.log('logged'); this.end(); process.stdin.once('data', function (d2) { console.log('never logged') }) }). This logs "logged", then waits for me to press Enter. When I press Enter, the process terminates. – Shawn Sep 24 '14 at 09:00