Search code examples
node.jsexpressspawnchild-process

nodejs/express - stream stdout instantly to the client


I spawned the following child: var spw = spawn('ping', ['-n','10', '127.0.0.1']) and I would like to receive the ping results on the client side (browser) one by one, not as a whole.

So far I tried this:

app.get('/path', function(req, res) {
   ...
   spw.stdout.on('data', function (data) {
      var str = data.toString();
      res.write(str + "\n");
   });
   ...
}

and that:

...
spw.stdout.pipe(res);
...

In both cases browser waits 10 of the pings to complete, and then prints the result as a whole. I would like to have them one by one, how to accomplish that?

(Client is just making a call to .../path and console.logs the result)


EDIT: Although I do believe that websockets are necessary to implement this, I just want to know whether there are any other ways. I saw several confusing SO answers, and blog posts (in this post, at step one OP streams the logs to the browser) which didn't help, therefore I decided to go for a bounty for some attention.


Solution

  • Here's a complete example using SSE (Server sent events). This works in Firefox and probably Chrome too:

    var cp = require("child_process"),
             express = require("express"),
             app = express();
    
    app.configure(function(){
        app.use(express.static(__dirname));
    });
    
    
    app.get('/msg', function(req, res){
        res.writeHead(200, { "Content-Type": "text/event-stream",
                             "Cache-control": "no-cache" });
    
        var spw = cp.spawn('ping', ['-c', '100', '127.0.0.1']),
        str = "";
    
        spw.stdout.on('data', function (data) {
            str += data.toString();
    
            // just so we can see the server is doing something
            console.log("data");
    
            // Flush out line by line.
            var lines = str.split("\n");
            for(var i in lines) {
                if(i == lines.length - 1) {
                    str = lines[i];
                } else{
                    // Note: The double-newline is *required*
                    res.write('data: ' + lines[i] + "\n\n");
                }
            }
        });
    
        spw.on('close', function (code) {
            res.end(str);
        });
    
        spw.stderr.on('data', function (data) {
            res.end('stderr: ' + data);
        });
    });
    
    app.listen(4000);
    

    And the client HTML:

    <!DOCTYPE Html>
    <html> 
    <body>
       <ul id="eventlist"> </ul>
    
       <script>              
        var eventList = document.getElementById("eventlist");
        var evtSource = new EventSource("http://localhost:4000/msg");
    
        var newElement = document.createElement("li");
        newElement.innerHTML = "Messages:";
        eventList.appendChild(newElement);
    
    
        evtSource.onmessage = function(e) {
            console.log("received event");
            console.log(e);
            var newElement = document.createElement("li");
    
            newElement.innerHTML = "message: " + e.data;
            eventList.appendChild(newElement);
        };      
    
        evtSource.onerror = function(e) {
            console.log("EventSource failed.");
        };
    
        console.log(evtSource);
    
        </script>
    
    </body>
    </html>
    

    Run node index.js and point your browser at http://localhost:4000/client.html. Note that I had to use the "-c" option rather than "-n" since I'm running OS X.