Repository navigation
Add the ability to wire-up listeners before starting a child process #38081
Description
Is your feature request related to a problem? Please describe.
When registering multiple listeners (callbacks) to the data event of a child process's stdout, there's no way to get the child process to wait for all the callbacks to be registered before starting. This means there's a window between registering the first listener and the second one in which the first listener might "pull" the first available chunk and when the second listener is registered, it won't receive the first chunk.
A thinned-down example:
// Expected behaviour scenario
const { exec } = require("child_process");
p = exec("seq 1000"); // This command prints the numbers between 1 and 1000, each in a different line
a1 = '';
a2 = '';
p.stdout.on("data", (d) => a1 += d);
p.stdout.on("data", (d) => a2 += d);
// When the child process completes, a1 and a2 will both contain all the numbers from 1 to 1000// Edge-case scenario
const { exec } = require("child_process");
p = exec("seq 1000");
a1 = '';
a2 = '';
p.stdout.on("data", (d) => a1 += d);
setTimeout(() => {
p.stdout.on("data", (d) => a2 += d);
}, 500);
// When the child process completes, a1 will contain all the numbers from 1 to 1000 while a2 will remain an empty stringFrom what I understand from reading the documentation of child_process, when a child process is started nodejs saves it's output in a buffer until a listener is registered (either by directly binding to the 'data' event or by pipe()ing stdout to a writable stream). This behaviour creates two potential problems:
- A second listener might not get the same data as the first one.
- the child process might output more data than the buffer can contain before any data can be processed.
Describe the solution you'd like
The solution I propose is to allow wiring up all the listeners and pipes before starting the child process.
Considering backwards compatibility, I imagine the best way to achieve this is by passing a new option (something like autostart that will default to true) to the options parameter of spawn, exec etc., that will make those functions return a ChildProcess instance that was not yet started, together with a new start() method added to the ChildProcess class.
Describe alternatives you've considered
The alternatives as I see them are:
- Only register a single handler and pass the data around to your multiple destinations.
- Try to proxy the readable stream to a second one that is already wired up.
- Try your best to minimize that window and hope for the best.
Activity
Trott commentedon Apr 6, 2021
If I'm understanding you correctly, there's no problem here. I think you might need to take some time to understand the event loop.
In this code that you provide, the listeners are wired up before the child process is able to emit any events
This will always exhibit the expected behavior because "data" events cannot fire until the current tick in the event loop is done. So both listeners will always receive the same events. No
'data'events can be emitted until all the code above runs. Only then will'data'events be emitted. Events that might normally be fired while the code is running are queued up and emitted after.On the other hand:
Given this code, I would not expect
a1anda2to have the same values. This is expected behavior and not an edge case.setTimeout()schedules the event for later and returns control to the event loop (because it's the last command). So events start firing. At some point, the timer code runs and only then isa2picking up events.Trott commentedon Apr 6, 2021
At least in the sample code you provide, there is no window. No need to hope for the best. Even this works as expected:
I'm going to go ahead and close this, but feel free to comment or re-open if I've misunderstood the issue here. Thanks for requesting a feature!
TheYarin commentedon Apr 6, 2021
So you're saying the user should trust that events registered will not be fired until there's a context switch. Alright, if that's so inherent in nodejs programming then you're right, the edge case I described is not inherently a problem.
And still, I can't seem to shake the feeling that just spawning a process to the air without allowing any prior wireup is a bad idea.
What if there's a heavy operation to be done between the spawning and the wireup? In that case problem 2 (the child process might output more data than the buffer can contain before any data can be processed) may still be an issue.
And what if I need some context changes between the spawning and the wireup? Of course you could say, "just move the wireup closer to the spawning!", but that might complicate the code's design by adding an additional unnecessary constraint. (unnecessary given the solution I proposed)
This is just my hunch, I have no concrete examples here. What do you think?
Trott commentedon Apr 7, 2021
Yes, because if not, then the event loop is broken and nothing will work the way it is supposed to. This is similar to the way the user also needs to trust that the first line of the file is parsed and executed before the second line. That analogy is an exaggeration, but much less of one than one might think.
Like this? Still works as expected.
I don't believe that is correct. https://stackoverflow.com/a/35464318/436641
TheYarin commentedon Apr 8, 2021
Since your example didn't quite stress the default
maxBuffersize (1024*1024), I thought of a better example to demonstrate problem 2:But surprisingly, the buffer limit simply didn't kick in.
The docs say (regarding
maxBuffer):But in reality, setting a low
maxBuffervalue and pumping more output than it should have been able to handle just worked.So.. now I'm just confused.
Trott commentedon Apr 8, 2021
I imagine it's working because
maxBufferapplies to theChildProcessobject returned byexec()(in your case,p) but does not directly cause the underlying'data'event onp.stdoutto truncate. But just in case I'm glossing over things and there's a bug here or something: Hey, @nodejs/child_process, is that correct? It does seem a bit surprising, although perhaps not once you realizep.stdoutis a stream?Anyway, @TheYarin, this will get the truncated
stdoutyou were expecting:Adding in the long
forloop doesn't change the result other than making you wait a while to get it because the event loop is blocked.