Why doesn't my Node.js process terminate once all listeners have been removed?
Asked Answered
S

2

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?

Sensuous answered 23/9, 2014 at 20:51 Comment(0)
F
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).

Farcical answered 23/9, 2014 at 21:8 Comment(12)
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.Sensuous
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.Farcical
Why then does it keep the process open the first time around?Sensuous
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.Farcical
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).Farcical
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.Sensuous
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, or promises. With either of those you can get a callback when all your async tasks are complete; in there you can call unref().Farcical
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. @Sensuous there's clearly some fundamental knowledge you're lacking if you're trying to attach event listeners to completed (ended) streams.Yippie
@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?Sensuous
I have been wanting to find something like process._getActiveHandles() and process._getActiveRequests() for years and somehow never came across them. Thanks!Ettore
end() is not a documented solution. process.stdin is documented as a Readable Stream. end() is a method on Writable Stream.Amaya
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().Ramsey
Y
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.

Yippie answered 23/9, 2014 at 21:21 Comment(2)
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.Sensuous
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.Sensuous

© 2022 - 2024 — McMap. All rights reserved.