Mercurial > hg > rlgwebd
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 } |
