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 } |