Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 738
anvarik Avatar asked Feb 21 '14 12:02

anvarik


3 Answers

If you are using Google Chrome, changing the content-type to "text/event-stream" does what your looking for.

res.writeHead(200, { "Content-Type": "text/event-stream" });

See my gist for complete example: https://gist.github.com/sfarthin/9139500

like image 23
Steve Farthing Avatar answered Sep 29 '22 02:09

Steve Farthing


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.

like image 195
nimrodm Avatar answered Sep 29 '22 04:09

nimrodm


This cannot be achieved with the standard HTTP request/response cycle. Basically what you are trying to do is make a "push" or "realtime" server. This can only be achieved with xhr-polling or websockets.

Code Example 1:

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

This code never sends an end signal and therefore will never respond. If you were to add a call to res.end() within that event handler, you will only get the first ping – which is the expected behavior because you are ending the response stream after the first chunk of data from stdout.

Code Sample 2:

spw.stdout.pipe(res);

Here stdout is flushing the packets to the browser, but the browser will not render the data chunks until all packets are received. Thus the reason why it waits 10 seconds and then renders the entirety of stdout. The major benefit to this method is not buffering the response in memory before sending — keeping your memory footprint lightweight.

like image 25
srquinn Avatar answered Sep 29 '22 02:09

srquinn