comparison rlgwebd.js @ 159:a613380ffdc2

RLGWebD: excise polling. WebSockets are supported nearly everywhere now. Listing current games and watching them are still broken.
author John "Elwin" Edwards
date Sat, 03 Jan 2015 15:23:04 -0500
parents 9961a538c00e
children ed837da65e5f
comparison
equal deleted inserted replaced
158:9961a538c00e 159:a613380ffdc2
61 }; 61 };
62 62
63 /* Global state */ 63 /* Global state */
64 var logins = {}; 64 var logins = {};
65 var sessions = {}; 65 var sessions = {};
66 var clients = {};
67 var dglgames = {}; 66 var dglgames = {};
68 var allowlogin = true; 67 var allowlogin = true;
69 var gamemux = new events.EventEmitter(); 68 var gamemux = new events.EventEmitter();
70 69
71 /* Constructor. A TermSession handles a pty and the game running on it. 70 /* Constructor. A TermSession handles a pty and the game running on it.
191 this.term.kill('SIGHUP'); 190 this.term.kill('SIGHUP');
192 }; 191 };
193 } 192 }
194 TermSession.prototype = new events.EventEmitter(); 193 TermSession.prototype = new events.EventEmitter();
195 194
196 function Watcher(session) {
197 var ss = this; // that
198 this.session = session;
199 this.alive = true;
200 /* State for messaging. */
201 this.nsend = 0;
202 this.sendQ = [];
203 /* Get a place in the table. */
204 this.id = randkey(2);
205 while (this.id in clients) {
206 this.id = randkey(2);
207 }
208 clients[this.id] = this;
209 /* Recreate the current screen state from the session's buffer. */
210 this.sendQ.push({"t": "d", "n": this.nsend++,
211 "d": session.framebuf.toString("hex", 0, session.frameoff)});
212 function dataH(buf) {
213 var reply = {};
214 reply.t = "d";
215 reply.n = ss.nsend++;
216 reply.d = buf.toString("hex");
217 ss.sendQ.push(reply);
218 }
219 function exitH() {
220 ss.alive = false;
221 ss.sendQ.push({"t": "q"});
222 }
223 session.on('data', dataH);
224 session.on('exit', exitH);
225 this.read = function() {
226 /* Returns an array of all outstanding messages, empty if none. */
227 var temp = this.sendQ;
228 this.sendQ = [];
229 /* Clean up if finished. */
230 if (!this.alive) {
231 delete clients[this.id];
232 }
233 return temp;
234 };
235 this.quit = function() {
236 this.session.removeListener('data', dataH);
237 this.session.removeListener('exit', exitH);
238 delete clients[this.id];
239 };
240 }
241
242 function Player(gamename, lkey, dims, callback) {
243 var ss = this;
244 this.alive = false;
245 /* State for messaging. */
246 this.nsend = 0;
247 this.nrecv = 0;
248 this.sendQ = [];
249 this.recvQ = []
250 this.Qtimeout = null;
251 /* Get a place in the table. */
252 this.id = randkey(2);
253 while (this.id in clients) {
254 this.id = randkey(2);
255 }
256 clients[this.id] = this;
257
258 this.read = function() {
259 var temp = this.sendQ;
260 this.sendQ = [];
261 /* Clean up if finished. */
262 if (!this.alive) {
263 clearTimeout(this.Qtimeout);
264 delete clients[this.id];
265 }
266 return temp;
267 };
268 this.write = function (data, n) {
269 if (!this.alive || typeof (n) != "number") {
270 return;
271 }
272 var oindex = n - this.nrecv;
273 if (oindex === 0) {
274 this.session.write(data);
275 this.nrecv++;
276 var next;
277 while ((next = this.recvQ.shift()) !== undefined) {
278 this.session.write(next);
279 this.nrecv++;
280 }
281 if (this.recvQ.length == 0 && this.Qtimeout) {
282 clearTimeout(this.Qtimeout);
283 this.Qtimeout = null;
284 }
285 }
286 else if (oindex > 0 && oindex <= 1024) {
287 tslog("Client %s: Stashing message %d at %d", this.id, n, oindex - 1);
288 this.recvQ[oindex - 1] = data;
289 if (!this.Qtimeout) {
290 var nextn = this.nrecv + this.recvQ.length + 1;
291 this.Qtimeout = setTimeout(this.flushQ, 30000, this, nextn);
292 }
293 }
294 /* Otherwise, discard it */
295 return;
296 };
297 this.flushQ = function (client, n) {
298 /* Callback for when an unreceived message times out.
299 * n is the first empty space that will not be given up on. */
300 if (!client.alive || client.nrecv >= n)
301 return;
302 client.nrecv++;
303 var next;
304 /* Clear the queue up to n */
305 while (client.nrecv < n) {
306 next = client.recvQ.shift();
307 if (next !== undefined)
308 client.session.write(next);
309 client.nrecv++;
310 }
311 /* Clear out anything that's ready. */
312 while ((next = client.recvQ.shift()) !== undefined) {
313 client.session.write(next);
314 client.nrecv++;
315 }
316 /* Now set another timeout if necessary. */
317 if (client.recvQ.length != 0) {
318 var nextn = client.nrecv + client.recvQ.length + 1;
319 client.Qtimeout = setTimeout(client.flushQ, 30000, client, nextn);
320 }
321 tslog("Flushing queue for player %s", player.id);
322 };
323 this.reset = function () {
324 /* To be called when the game is taken over. */
325 if (this.Qtimeout) {
326 clearTimeout(this.Qtimeout);
327 this.Qtimeout = null;
328 }
329 for (var i = 0; i < this.recvQ.length; i++) {
330 if (this.recvQ[i] !== undefined) {
331 this.session.write(this.recvQ[i]);
332 }
333 }
334 this.recvQ = [];
335 this.nrecv = 0;
336 this.nsend = 0;
337 this.sendQ = [{"t": "d", "n": this.nsend++,
338 "d": this.session.framebuf.toString("hex", 0, this.session.frameoff)}];
339 };
340 this.quit = function() {
341 if (this.alive)
342 this.session.close();
343 };
344 function openH(success, tag) {
345 if (success) {
346 ss.alive = true;
347 ss.session = sessions[tag];
348 ss.h = sessions[tag].h;
349 ss.w = sessions[tag].w;
350 }
351 callback(ss, success);
352 }
353 function dataH(chunk) {
354 var reply = {};
355 reply.t = "d";
356 reply.n = ss.nsend++;
357 reply.d = chunk.toString("hex");
358 ss.sendQ.push(reply);
359 }
360 function exitH() {
361 ss.alive = false;
362 ss.sendQ.push({"t": "q"});
363 }
364 var handlers = {'open': openH, 'data': dataH, 'exit': exitH};
365 this.session = new TermSession(gamename, lkey, dims, handlers);
366 }
367
368 // Also known as WebSocketAndTermSessionClosureGlueFactory 195 // Also known as WebSocketAndTermSessionClosureGlueFactory
369 function wsWatcher(conn, session) { 196 function wsWatcher(conn, session) {
370 var ss = this; // is this even needed? 197 var ss = this; // is this even needed?
371 var dataH = function(buf) { 198 var dataH = function(buf) {
372 conn.sendUTF(JSON.stringify({"t": "d", "d": buf.toString("hex")})); 199 conn.sendUTF(JSON.stringify({"t": "d", "d": buf.toString("hex")}));
714 pwchecker.on("exit", checkit); 541 pwchecker.on("exit", checkit);
715 pwchecker.stdin.end(username + '\n' + password + '\n', "utf8"); 542 pwchecker.stdin.end(username + '\n' + password + '\n', "utf8");
716 return; 543 return;
717 } 544 }
718 545
719 function startgame(req, res, formdata) {
720 if (!allowlogin) {
721 sendError(res, 6, null);
722 return;
723 }
724 if (!("key" in formdata)) {
725 sendError(res, 2, "No key given.");
726 return;
727 }
728 else if (!("game" in formdata)) {
729 sendError(res, 2, "No game specified.");
730 return;
731 }
732 var lkey = String(formdata["key"]);
733 if (!(lkey in logins)) {
734 sendError(res, 1, null);
735 return;
736 }
737 var username = logins[lkey].name;
738 var gname = formdata["game"];
739 // If dims are not given or invalid, the constructor will handle it.
740 var dims = [formdata["h"], formdata["w"]];
741 if (!(gname in games)) {
742 sendError(res, 2, "No such game: " + gname);
743 tslog("Request for nonexistant game \"%s\"", gname);
744 return;
745 }
746 // A callback to pass to the game-in-progress checker.
747 var launch = function(err, fname) {
748 var nodematch = new RegExp("^" + username + ":node:");
749 if (fname && (fname.match(nodematch) === null)) {
750 /* It's being played in dgamelaunch. */
751 sendError(res, 4, "dgamelaunch");
752 tslog("%s is already playing %s", username, gname);
753 return;
754 }
755 // Game starting has been approved.
756 var respondlaunch = function(nclient, success) {
757 if (success) {
758 res.writeHead(200, {'Content-Type': 'application/json'});
759 var reply = {"t": "s", "id": nclient.id, "w": nclient.w, "h":
760 nclient.h, "p": username, "g": gname};
761 res.write(JSON.stringify(reply));
762 res.end();
763 }
764 else {
765 sendError(res, 5, "Failed to open TTY");
766 tslog("Unable to allocate TTY for %s", gname);
767 }
768 };
769 if (fname) {
770 for (var cid in clients) {
771 cli = clients[cid];
772 if ((cli instanceof Player) &&
773 cli.session.pname == username &&
774 cli.session.game.uname == gname) {
775 cli.reset();
776 respondlaunch(cli, true);
777 tslog("Game %d has been taken over.", cli.session.sessid);
778 return;
779 }
780 }
781 /* If there's no player, it's a WebSocket game, and shouldn't be
782 * seized. */
783 sendError(res, 4, "WebSocket");
784 }
785 else {
786 new Player(gname, lkey, dims, respondlaunch);
787 }
788 };
789 checkprogress(username, games[gname], launch, []);
790 }
791
792 function watch(req, res, formdata) {
793 if (!("g" in formdata) | !("p" in formdata)) {
794 sendError(res, 2, "Game or player not given");
795 return;
796 }
797 if (!(formdata.g in games)) {
798 sendError(res, 2, "No such game: " + formdata.g);
799 return;
800 }
801 var tag = formdata.g = "/" + formdata.p;
802 if (!(tag in sessions)) {
803 sendError(res, 7);
804 return;
805 }
806 var session = sessions[tag];
807 var watch = new Watcher(session);
808 var reply = {"t": "w", "w": session.w, "h": session.h,
809 "p": session.pname, "g": session.game.uname};
810 res.writeHead(200, {'Content-Type': 'application/json'});
811 res.write(JSON.stringify(reply));
812 res.end();
813 tslog("Game %d is being watched", tag);
814 }
815
816 /* Sets things up for a new user, like dgamelaunch's commands[register] */ 546 /* Sets things up for a new user, like dgamelaunch's commands[register] */
817 function regsetup(username) { 547 function regsetup(username) {
818 function regsetup_l2(err) { 548 function regsetup_l2(err) {
819 for (var g in games) { 549 for (var g in games) {
820 fs.mkdir(path.join("/dgldir/ttyrec", username, games[g].uname), 0755); 550 fs.mkdir(path.join("/dgldir/ttyrec", username, games[g].uname), 0755);
874 child_adder.on("exit", checkreg); 604 child_adder.on("exit", checkreg);
875 child_adder.stdin.end(uname + '\n' + passwd + '\n' + email + '\n', "utf8"); 605 child_adder.stdin.end(uname + '\n' + passwd + '\n' + email + '\n', "utf8");
876 return; 606 return;
877 } 607 }
878 608
879 /* Ends the game, obviously. Less obviously, stops watching the game if
880 * the client is a Watcher instead of a Player. */
881 function endgame(client, res) {
882 if (!client.alive) {
883 sendError(res, 7, null, true);
884 return;
885 }
886 client.quit();
887 // Give things some time to happen.
888 if (client instanceof Player)
889 setTimeout(readFeed, 200, client, res);
890 else
891 readFeed(client, res);
892 return;
893 }
894
895 /* Stops a running game if the request has the proper key. */ 609 /* Stops a running game if the request has the proper key. */
896 /* TODO does this still work? */
897 function stopgame(res, formdata) { 610 function stopgame(res, formdata) {
898 if (!("key" in formdata) || !(formdata["key"] in logins)) { 611 if (!("key" in formdata) || !(formdata["key"] in logins)) {
899 sendError(res, 1); 612 sendError(res, 1);
900 return; 613 return;
901 } 614 }
936 res.write(JSON.stringify({"t": "q"})); 649 res.write(JSON.stringify({"t": "q"}));
937 res.end(); 650 res.end();
938 }); 651 });
939 } 652 }
940 checkprogress(pname, games[gname], checkback, []); 653 checkprogress(pname, games[gname], checkback, []);
941 }
942
943 /* TODO remove polling */
944 function findClient(formdata, playersOnly) {
945 if (typeof(formdata) != "object")
946 return null;
947 if ("id" in formdata) {
948 var id = formdata["id"];
949 if (id in clients && (!playersOnly || clients[id] instanceof Player)) {
950 return clients[id];
951 }
952 }
953 return null;
954 } 654 }
955 655
956 function startProgressWatcher() { 656 function startProgressWatcher() {
957 var watchdirs = []; 657 var watchdirs = [];
958 for (var gname in games) { 658 for (var gname in games) {
1030 } 730 }
1031 }); 731 });
1032 return; 732 return;
1033 } 733 }
1034 734
1035 /* TODO remove polling */
1036 function readFeed(client, res) {
1037 if (!client) {
1038 sendError(res, 7, null, true);
1039 return;
1040 }
1041 var msgs = client.read();
1042 if (!allowlogin && !msgs.length) {
1043 sendError(res, 6, null, true);
1044 return;
1045 }
1046 res.writeHead(200, { "Content-Type": "application/json" });
1047 res.write(JSON.stringify(msgs));
1048 res.end();
1049 }
1050
1051 /* TODO simplify by storing timestamps instead of callin stat() */ 735 /* TODO simplify by storing timestamps instead of callin stat() */
1052 function getStatus(callback) { 736 function getStatus(callback) {
1053 var now = new Date(); 737 var now = new Date();
1054 var statusinfo = {"s": allowlogin, "g": []}; 738 var statusinfo = {"s": allowlogin, "g": []};
1055 function idleset(n, idletime) { 739 function idleset(n, idletime) {
1270 function respond() { 954 function respond() {
1271 formdata = getMsg(reqbody); 955 formdata = getMsg(reqbody);
1272 var target = url.parse(req.url).pathname; 956 var target = url.parse(req.url).pathname;
1273 /* First figure out if the client is POSTing to a command interface. */ 957 /* First figure out if the client is POSTing to a command interface. */
1274 if (req.method == 'POST') { 958 if (req.method == 'POST') {
1275 if (target == '/feed') { 959 if (target == "/login") {
1276 var client = findClient(formdata, false);
1277 if (!client) {
1278 sendError(res, 7, null, true);
1279 return;
1280 }
1281 if (formdata.t == "q") {
1282 /* The client wants to terminate the process. */
1283 endgame(client, res);
1284 return; // endgame() calls readFeed() itself.
1285 }
1286 else if (formdata.t == "d" && typeof(formdata.d) == "string") {
1287 if (!(client instanceof Player)) {
1288 sendError(res, 7, "Watching", true);
1289 return;
1290 }
1291 /* process the keys */
1292 var hexstr = formdata.d.replace(/[^0-9a-f]/gi, "");
1293 if (hexstr.length % 2 != 0) {
1294 sendError(res, 2, "incomplete byte", true);
1295 return;
1296 }
1297 var keybuf = new Buffer(hexstr, "hex");
1298 client.write(keybuf, formdata.n);
1299 }
1300 readFeed(client, res);
1301 }
1302 else if (target == "/login") {
1303 login(req, res, formdata); 960 login(req, res, formdata);
1304 } 961 }
1305 else if (target == "/addacct") { 962 else if (target == "/addacct") {
1306 register(req, res, formdata); 963 register(req, res, formdata);
1307 }
1308 else if (target == "/play") {
1309 startgame(req, res, formdata);
1310 }
1311 else if (target == "/watch") {
1312 watch(req, res, formdata);
1313 } 964 }
1314 else if (target == "/quit") { 965 else if (target == "/quit") {
1315 stopgame(res, formdata); 966 stopgame(res, formdata);
1316 } 967 }
1317 else if (target.match(/^\/uinfo\//)) { 968 else if (target.match(/^\/uinfo\//)) {
1321 res.writeHead(405, resheaders); 972 res.writeHead(405, resheaders);
1322 res.end(); 973 res.end();
1323 } 974 }
1324 } 975 }
1325 else if (req.method == 'GET' || req.method == 'HEAD') { 976 else if (req.method == 'GET' || req.method == 'HEAD') {
1326 if (target == '/feed') { 977 if (target == '/status') {
1327 if (req.method == 'HEAD') {
1328 res.writeHead(200, {"Content-Type": "application/json"});
1329 res.end();
1330 }
1331 else
1332 sendError(res, 7, null, true);
1333 return;
1334 }
1335 else if (target == '/status') {
1336 statusmsg(req, res); 978 statusmsg(req, res);
1337 } 979 }
1338 else if (target.match(/^\/uinfo\//)) { 980 else if (target.match(/^\/uinfo\//)) {
1339 getuinfo(req, res); 981 getuinfo(req, res);
1340 } 982 }
1350 res.end(); 992 res.end();
1351 } 993 }
1352 return; 994 return;
1353 } 995 }
1354 req.on('end', respond); 996 req.on('end', respond);
1355
1356 } 997 }
1357 998
1358 function wsHandler(wsRequest) { 999 function wsHandler(wsRequest) {
1359 var watchmatch = wsRequest.resource.match(/^\/watch\/(.*)$/); 1000 var watchmatch = wsRequest.resource.match(/^\/watch\/(.*)$/);
1360 var playmatch = wsRequest.resource.match(/^\/play\//); 1001 var playmatch = wsRequest.resource.match(/^\/play\//);
1401 } 1042 }
1402 else 1043 else
1403 wsRequest.reject(404, "No such resource."); 1044 wsRequest.reject(404, "No such resource.");
1404 } 1045 }
1405 1046
1047 /* TODO use a list instead */
1406 function pushStatus() { 1048 function pushStatus() {
1407 getStatus(function(info) { 1049 getStatus(function(info) {
1408 info["t"] = "t"; 1050 info["t"] = "t";
1409 gamemux.emit('list', info); 1051 gamemux.emit('list', info);
1410 }); 1052 });