c970c00ce7b418f01232380bf093d985523675ea
[remoteglot] / www / serve-analysis.js
1 // node.js version of analysis.pl; hopefully scales a bit better
2 // for this specific kind of task.
3
4 // Modules.
5 var http = require('http');
6 var fs = require('fs');
7 var url = require('url');
8 var querystring = require('querystring');
9 var path = require('path');
10 var zlib = require('zlib');
11
12 // Constants.
13 var json_filename = '/srv/analysis.sesse.net/www/analysis.json';
14
15 // The current contents of the file to hand out, and its last modified time.
16 var json = undefined;
17
18 // The list of clients that are waiting for new data to show up.
19 // Uniquely keyed by request_id so that we can take them out of
20 // the queue if they close the socket.
21 var sleeping_clients = {};
22 var request_id = 0;
23
24 // List of when clients were last seen, keyed by their unique ID.
25 // Used to show a viewer count to the user.
26 var last_seen_clients = {};
27
28 // The timer used to touch the file every 30 seconds if nobody
29 // else does it for us. This makes sure we don't have clients
30 // hanging indefinitely (which might have them return errors).
31 var touch_timer = undefined;
32
33 // If we are behind Varnish, we can't count the number of clients
34 // ourselves, so some external log-tailing daemon needs to tell us.
35 var viewer_count_override = undefined;
36
37 var replace_json = function(new_json_contents, mtime) {
38         var new_json = {
39                 parsed: JSON.parse(new_json_contents),
40                 plain: new_json_contents,
41                 last_modified: mtime
42         };
43
44         // gzip the new version, and put it into place.
45         zlib.gzip(new_json_contents, function(err, buffer) {
46                 if (err) throw err;
47
48                 new_json.gzip = buffer;
49                 json = new_json;
50
51                 // Finally, wake up any sleeping clients.
52                 possibly_wakeup_clients();
53         });
54 }
55
56 var reread_file = function(event, filename) {
57         if (filename != path.basename(json_filename)) {
58                 return;
59         }
60         console.log("Rereading " + json_filename);
61         fs.open(json_filename, 'r+', function(err, fd) {
62                 if (err) throw err;
63                 fs.fstat(fd, function(err, st) {
64                         if (err) throw err;
65                         var buffer = new Buffer(1048576);
66                         fs.read(fd, buffer, 0, 1048576, 0, function(err, bytesRead, buffer) {
67                                 if (err) throw err;
68                                 fs.close(fd, function() {
69                                         var new_json_contents = buffer.toString('utf8', 0, bytesRead);
70                                         replace_json(new_json_contents, st.mtime.getTime());
71                                 });
72                         });
73                 });
74         });
75
76         if (touch_timer !== undefined) {
77                 clearTimeout(touch_timer);
78         }
79         touch_timer = setTimeout(function() {
80                 console.log("Touching analysis.json due to no other activity");
81                 var now = Date.now() / 1000;
82                 fs.utimes(json_filename, now, now);
83         }, 30000);
84 }
85 var possibly_wakeup_clients = function() {
86         var num_viewers = count_viewers();
87         for (var i in sleeping_clients) {
88                 mark_recently_seen(sleeping_clients[i].unique);
89                 send_json(sleeping_clients[i].response,
90                           sleeping_clients[i].accept_gzip,
91                           num_viewers);
92         }
93         sleeping_clients = {};
94 }
95 var send_404 = function(response) {
96         response.writeHead(404, {
97                 'Content-Type': 'text/plain',
98         });
99         response.write('Something went wrong. Sorry.');
100         response.end();
101 }
102 var handle_viewer_override = function(request, u, response) {
103         // Only accept requests from localhost.
104         var peer = request.socket.localAddress;
105         if ((peer != '127.0.0.1' && peer != '::1') || request.headers['x-forwarded-for']) {
106                 console.log("Refusing viewer override from " + peer);
107                 send_404(response);
108         } else {
109                 viewer_count_override = (u.query)['num'];
110                 response.writeHead(200, {
111                         'Content-Type': 'text/plain',
112                 });
113                 response.write('OK.');
114                 response.end();
115         }
116 }
117 var send_json = function(response, accept_gzip, num_viewers) {
118         var headers = {
119                 'Content-Type': 'text/json',
120                 'X-Remoteglot-Last-Modified': json.last_modified,
121                 'X-Remoteglot-Num-Viewers': num_viewers,
122                 'Access-Control-Expose-Headers': 'X-Remoteglot-Last-Modified, X-Remoteglot-Num-Viewers',
123                 'Expires': 'Mon, 01 Jan 1970 00:00:00 UTC',
124                 'Vary': 'Accept-Encoding',
125         };
126
127         if (accept_gzip) {
128                 headers['Content-Encoding'] = 'gzip';
129                 response.writeHead(200, headers);
130                 response.write(json.gzip);
131         } else {
132                 response.writeHead(200, headers);
133                 response.write(json.text);
134         }
135         response.end();
136 }
137 var mark_recently_seen = function(unique) {
138         if (unique) {
139                 last_seen_clients[unique] = (new Date).getTime();
140         }
141 }
142 var count_viewers = function() {
143         if (viewer_count_override !== undefined) {
144                 return viewer_count_override;
145         }
146
147         var now = (new Date).getTime();
148
149         // Go through and remove old viewers, and count them at the same time.
150         var new_last_seen_clients = {};
151         var num_viewers = 0;
152         for (var unique in last_seen_clients) {
153                 if (now - last_seen_clients[unique] < 5000) {
154                         ++num_viewers;
155                         new_last_seen_clients[unique] = last_seen_clients[unique];
156                 }
157         }
158
159         // Also add sleeping clients that we would otherwise assume timed out.
160         for (var request_id in sleeping_clients) {
161                 var unique = sleeping_clients[request_id].unique;
162                 if (unique && !(unique in new_last_seen_clients)) {
163                         ++num_viewers;
164                 }
165         }
166
167         last_seen_clients = new_last_seen_clients;
168         return num_viewers;
169 }
170
171 // Set up a watcher to catch changes to the file, then do an initial read
172 // to make sure we have a copy.
173 fs.watch(path.dirname(json_filename), reread_file);
174 reread_file(null, path.basename(json_filename));
175
176 var server = http.createServer();
177 server.on('request', function(request, response) {
178         var u = url.parse(request.url, true);
179         var ims = (u.query)['ims'];
180         var unique = (u.query)['unique'];
181
182         console.log((new Date).getTime()*1e-3 + " " + request.url);
183         if (u.pathname === '/override-num-viewers') {
184                 handle_viewer_override(request, u, response);
185                 return;
186         }
187         if (u.pathname !== '/analysis.pl') {
188                 // This is not the request you are looking for.
189                 send_404(response);
190                 return;
191         }
192
193         mark_recently_seen(unique);
194
195         var accept_encoding = request.headers['accept-encoding'];
196         var accept_gzip;
197         if (accept_encoding !== undefined && accept_encoding.match(/\bgzip\b/)) {
198                 accept_gzip = true;
199         } else {
200                 accept_gzip = false;
201         }
202
203         // If we already have something newer than what the user has,
204         // just send it out and be done with it.
205         if (json !== undefined && (!ims || json.last_modified > ims)) {
206                 send_json(response, accept_gzip, count_viewers());
207                 return;
208         }
209
210         // OK, so we need to hang until we have something newer.
211         // Put the user on the wait list.
212         var client = {};
213         client.response = response;
214         client.request_id = request_id;
215         client.accept_gzip = accept_gzip;
216         client.unique = unique;
217         sleeping_clients[request_id++] = client;
218
219         request.socket.client = client;
220 });
221 server.on('connection', function(socket) {
222         socket.on('close', function() {
223                 var client = socket.client;
224                 if (client) {
225                         mark_recently_seen(client.unique);
226                         delete sleeping_clients[client.request_id];
227                 }
228         });
229 });
230 server.listen(5000);