# HG changeset patch # User John "Elwin" Edwards # Date 1420316584 18000 # Node ID a613380ffdc29dc44743c4525f60eb9d2ed11d42 # Parent 9961a538c00e7e6b150977feea5a2a56c9235bbe RLGWebD: excise polling. WebSockets are supported nearly everywhere now. Listing current games and watching them are still broken. diff -r 9961a538c00e -r a613380ffdc2 rlgterm.js --- a/rlgterm.js Thu Jan 01 15:56:22 2015 -0500 +++ b/rlgterm.js Sat Jan 03 15:23:04 2015 -0500 @@ -1,60 +1,5 @@ /* rlgterm.js: Roguelike Gallery's driver for termemu.js */ -// A state machine that keeps track of polling the server. -var ajaxstate = { - state: 0, - timerID: null, - clear: function () { - if (this.timerID != null) { - window.clearTimeout(this.timerID); - this.timerID = null; - } - }, - set: function (ms) { - this.clear(); - this.timerID = window.setTimeout(getData, ms); - }, - gotdata: function () { - this.set(1000); - this.state = 1; - }, - gotnothing: function () { - if (this.state == 0) { - this.set(1000); - this.state = 1; - } - else if (this.state < 4) { - this.set(4000); - this.state++; - } - else if (session.playing) { - if (this.state < 8) { - this.set(15000); - this.state++; - } - else { - /* It's been over a minute. Stop polling. */ - this.clear(); - } - } - else { - /* If watching, it can't stop polling entirely, because there - * are no POST events to start it up again. */ - this.set(10000); - } - }, - posted: function (wasdata) { - if (wasdata) { - this.set(1000); - this.state = 1; - } - else { - this.set(200); - this.state = 0; - } - } -}; - /* Data on the available games. */ var games = { "rogue3": { @@ -81,7 +26,7 @@ var session = { /* The session id assigned by the server. */ - id: null, + connect: false, /* Login name and key are now in sessionStorage. */ /* Whether the game is being played or just watched. */ playing: false, @@ -165,119 +110,15 @@ return; } -/* State for sending and receiving messages. */ -var nsend = 0; // The number of the next packet to send. -var nrecv = 0; // The next packet expected. -var msgQ = []; // Queue for out-of-order messages. - -/* Processes a message from the server, returning true or false if it was a - * data message with or without data, null if not data. - * All non-special responseTexts should be handed directly to this function. - */ -function processMsg(msg) { - var msgDicts; - var havedata = null; // eventual return value - try { - msgDicts = JSON.parse(msg); - } catch (e) { - if (e instanceof SyntaxError) - return null; - } - if (msgDicts.length === 0) - return false; - for (var j = 0; j < msgDicts.length; j++) { - if (!msgDicts[j].t) - continue; - else if (msgDicts[j].t == "E") { - if (msgDicts[j].c == 1 || msgDicts[j].c == 6 || msgDicts[j].c == 7) { - gameover(); - if (msgDicts[j].c == 1) { - logout(); - } - } - debug(1, "Server error: " + msgDicts[j].s); - } - // A data message - else if (msgDicts[j].t == "d") { - if (msgDicts[j].n === nrecv) { - writeData(msgDicts[j].d); - nrecv++; - /* Process anything in the queue that's now ready. */ - var next; - while ((next = msgQ.shift()) !== undefined) { - writeData(next.d); - nrecv++; - } - } - else if (msgDicts[j].n > nrecv) { - /* The current message comes after one still missing. Queue this one - * for later use. */ - debug(1, "Got packet " + msgDicts[j].n + ", expected " + nrecv); - msgQ[msgDicts[j].n - nrecv - 1] = msgDicts[j]; - } - else { - /* This message's number was encountered previously. */ - debug(1, "Discarding packet " + msgDicts[j].n + ", expected " + nrecv); - } - havedata = true; - } - else if (msgDicts[j].t == "T") { - setTitle(msgDicts[j].d); - } - else if (msgDicts[j].t == "q") { - gameover(); - } - else { - debug(1, "Unrecognized server message " + msg); - } - } - return havedata; -} - -function getData() { - if (session.id == null) - return; - var datareq = new XMLHttpRequest(); - var msg = JSON.stringify({"id": session.id, "t": "n"}); - datareq.onerror = errHandler; - datareq.onreadystatechange = function () { - if (datareq.readyState == 4 && datareq.status == 200) { - var wasdata = processMsg(datareq.responseText); - if (wasdata != null) { - if (wasdata) - ajaxstate.gotdata(); - else - ajaxstate.gotnothing(); - } - return; - } - }; - datareq.open('POST', '/feed', true); - datareq.send(msg); - return; -} - -function postResponseHandler() { - if (this.readyState == 4 && this.status == 200) { - // We might want to do something with wasdata someday. - var wasdata = processMsg(this.responseText); - ajaxstate.posted(); - return; - } -} - function errHandler() { message("Unable to connect to the server.", "warn"); } function sendback(str) { /* For responding to terminal queries. */ - var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": str}; - var datareq = new XMLHttpRequest(); - datareq.onerror = errHandler; - datareq.onreadystatechange = postResponseHandler; - datareq.open('POST', '/feed', true); - datareq.send(JSON.stringify(msgDict)); + if (session.sock) { + session.sock.send(JSON.stringify({"t": "d", "d": str})); + } return; } @@ -323,14 +164,7 @@ if (session.sock) { session.sock.send(JSON.stringify({"t": "d", "d": code})); } - else { - var datareq = new XMLHttpRequest(); - var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": code}; - datareq.onerror = errHandler; - datareq.onreadystatechange = postResponseHandler; - datareq.open('POST', '/feed', true); - datareq.send(JSON.stringify(msgDict)); - } + /* Otherwise it is disconnected */ return; } @@ -386,14 +220,6 @@ if (session.sock) { session.sock.send(JSON.stringify({"t": "d", "d": keystr})); } - else { - var datareq = new XMLHttpRequest(); - var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": keystr}; - datareq.onerror = errHandler; - datareq.onreadystatechange = postResponseHandler; - datareq.open('POST', '/feed', true); - datareq.send(JSON.stringify(msgDict)); - } return; } @@ -426,8 +252,8 @@ break; } if (!window.WebSocket) { - message("Your browser does not support WebSockets. You can still play, " + - "but it will be slower, and may not work in the future.", "warn"); + message("Your browser does not support WebSockets. " + + "This Web app will not work.", "warn"); } return; } @@ -454,8 +280,7 @@ function formlogin(ev) { ev.preventDefault(); - if (session.id != null) - return; + /* What to do if logged in already? */ var loginmsg = {}; loginmsg["name"] = document.getElementById("input_name").value; loginmsg["pw"] = document.getElementById("input_pw").value; @@ -490,6 +315,7 @@ return; } +/* FIXME game list API has changed */ function tableCurrent(gamelist) { var gamediv = document.getElementById("gametable"); while (gamediv.children.length > 2) @@ -533,7 +359,7 @@ function wsCurrent() { if (!window.WebSocket) return; - if (session.id) { + if (session.connect) { /* Don't bother with status if already playing/watching. */ if (statsock) { statsock.close(); @@ -569,11 +395,12 @@ } } +/* FIXME gamelist API has changed */ function getcurrent(clear) { if (window.WebSocket) { return; } - if (session.id || clear) { + if (session.connect || clear) { if (statInterval) { window.clearInterval(statInterval); statInterval = null; @@ -608,7 +435,7 @@ } function getchoices() { - if (session.id != null || !("lcred" in sessionStorage)) + if (session.connect || !("lcred" in sessionStorage)) return; var req = new XMLHttpRequest(); req.onerror = errHandler; @@ -680,62 +507,6 @@ return starter; } -function startgame(game) { - if (session.id != null || !("lcred" in sessionStorage)) - return; - if (window.WebSocket) { - wsStart(game); - return; - } - var smsg = {}; - smsg["key"] = sessionStorage.getItem("lcred"); - smsg["game"] = game.uname; - smsg["h"] = 24; - smsg["w"] = 80; - var req = new XMLHttpRequest(); - req.onerror = errHandler; - req.onreadystatechange = function () { - if (req.readyState != 4 || req.status != 200) - return; - var reply = JSON.parse(req.responseText); - if (reply.t == 's') { - /* Success */ - session.id = reply.id; - session.playing = true; - termemu.resize(reply.h, reply.w); - message("You are now playing " + game.name + "."); - setmode("play"); - getData(); - } - else if (reply.t == 'E') { - if (reply.c == 1) { - logout(); - message("The server forgot about you, please log in again.", "warn"); - } - else if (reply.c == 4) { - if (reply.s == "dgamelaunch") { - message("You are already playing " + game.name + " over SSH.", - "warn"); - } - else { - message("You are already playing " + game.name + - " in another browser window.", "warn"); - } - } - else if (reply.c == 7) { - message("The game is being saved, try again in a few seconds."); - } - else { - message("The server says it can't start your game because \"" + - reply.s + "\". This is probably a bug.", "warn"); - } - } - }; - req.open('POST', '/play', true); - req.send(JSON.stringify(smsg)); - return; -} - function makeStopper(gname) { if (!(gname in games)) return null; @@ -774,12 +545,17 @@ return; } -function wsStart(game) { +function startgame(game) { + if (!("lcred" in sessionStorage) || session.connect) + return; + if (!window.WebSocket) { + return; + } var sockurl = "ws://" + window.location.host + "/play/" + game.uname; sockurl += "?key=" + sessionStorage.getItem("lcred") + "&w=80&h=24"; ws = new WebSocket(sockurl); ws.onopen = function (event) { - session.id = true; + session.connect = true; session.playing = true; session.sock = ws; setmode("play"); @@ -800,55 +576,20 @@ }; } -function startwatching(gamenumber) { - if (session.id != null) - return; - if (window.WebSocket) { - wsWatch(gamenumber); - return; - } - var wmsg = {"n": Number(gamenumber)}; - var req = new XMLHttpRequest(); - req.onerror = errHandler; - req.onreadystatechange = function () { - if (req.readyState != 4 || req.status != 200) - return; - var reply = JSON.parse(req.responseText); - if (reply.t == 'w') { - /* Success */ - session.id = reply.id; - session.playing = false; - termemu.resize(reply.h, reply.w); - termemu.reset(); - termemu.toAltBuf(); - var pname = reply.p; - var gname = games[reply.g].name; - message("You are now watching " + pname + " play " + gname + "."); - setmode("watch"); - getData(); - } - else if (reply.t == 'E') { - message("The game could not be watched: " + reply.s, "warn"); - getcurrent(); - } - }; - req.open('POST', '/watch', true); - req.send(JSON.stringify(wmsg)); - return; -} - -function makeWatcher(n) { +function makeWatcher(t) { function watcher(ev) { - startwatching(n); + startwatching(t); } return watcher; } -function wsWatch(gamenumber) { - var sockurl = "ws://" + window.location.host + "/watch/" + String(gamenumber); +function startwatching(tag) { + if (session.connect) + return; + var sockurl = "ws://" + window.location.host + "/watch/" + tag; var ws = new WebSocket(sockurl); ws.onopen = function (event) { - session.id = true; + session.connect = true; session.sock = ws; setmode("watch"); }; @@ -874,7 +615,8 @@ function formreg(ev) { ev.preventDefault(); - if (session.id != null) + /* This ought to check for being logged in instead. */ + if (session.connect) return; var regmsg = {}; regmsg["name"] = document.getElementById("regin_name").value; @@ -925,20 +667,16 @@ } function gameover() { - if (session.id == null) + if (!session.connect) return; /* TODO IFACE2 If the end was unexpected, tell player the game was saved. */ if (session.playing) message("Finished playing."); else message("Finished watching."); - session.id = null; + session.connect = false; session.playing = false; - ajaxstate.clear(); termemu.toNormBuf(); - nsend = 0; - nrecv = 0; - msgQ = []; if ("lcred" in sessionStorage) setmode("choose"); else @@ -952,24 +690,14 @@ setmode("login"); } +/* TODO determine whether this is needed */ function stop() { - if (!session.id) + if (!session.connect) return; if (session.sock) { session.sock.close(); return; } - var req = new XMLHttpRequest(); - req.onerror = errHandler; - req.onreadystatechange = function () { - if (req.readyState == 4 && req.status == 200) { - processMsg(req.responseText); - return; - } - }; - req.open('POST', '/feed', true); - req.send(JSON.stringify({"id": session.id, "t": "q"})); - return; } function setmode(mode, ev) { diff -r 9961a538c00e -r a613380ffdc2 rlgwebd.js --- a/rlgwebd.js Thu Jan 01 15:56:22 2015 -0500 +++ b/rlgwebd.js Sat Jan 03 15:23:04 2015 -0500 @@ -63,7 +63,6 @@ /* Global state */ var logins = {}; var sessions = {}; -var clients = {}; var dglgames = {}; var allowlogin = true; var gamemux = new events.EventEmitter(); @@ -193,178 +192,6 @@ } TermSession.prototype = new events.EventEmitter(); -function Watcher(session) { - var ss = this; // that - this.session = session; - this.alive = true; - /* State for messaging. */ - this.nsend = 0; - this.sendQ = []; - /* Get a place in the table. */ - this.id = randkey(2); - while (this.id in clients) { - this.id = randkey(2); - } - clients[this.id] = this; - /* Recreate the current screen state from the session's buffer. */ - this.sendQ.push({"t": "d", "n": this.nsend++, - "d": session.framebuf.toString("hex", 0, session.frameoff)}); - function dataH(buf) { - var reply = {}; - reply.t = "d"; - reply.n = ss.nsend++; - reply.d = buf.toString("hex"); - ss.sendQ.push(reply); - } - function exitH() { - ss.alive = false; - ss.sendQ.push({"t": "q"}); - } - session.on('data', dataH); - session.on('exit', exitH); - this.read = function() { - /* Returns an array of all outstanding messages, empty if none. */ - var temp = this.sendQ; - this.sendQ = []; - /* Clean up if finished. */ - if (!this.alive) { - delete clients[this.id]; - } - return temp; - }; - this.quit = function() { - this.session.removeListener('data', dataH); - this.session.removeListener('exit', exitH); - delete clients[this.id]; - }; -} - -function Player(gamename, lkey, dims, callback) { - var ss = this; - this.alive = false; - /* State for messaging. */ - this.nsend = 0; - this.nrecv = 0; - this.sendQ = []; - this.recvQ = [] - this.Qtimeout = null; - /* Get a place in the table. */ - this.id = randkey(2); - while (this.id in clients) { - this.id = randkey(2); - } - clients[this.id] = this; - - this.read = function() { - var temp = this.sendQ; - this.sendQ = []; - /* Clean up if finished. */ - if (!this.alive) { - clearTimeout(this.Qtimeout); - delete clients[this.id]; - } - return temp; - }; - this.write = function (data, n) { - if (!this.alive || typeof (n) != "number") { - return; - } - var oindex = n - this.nrecv; - if (oindex === 0) { - this.session.write(data); - this.nrecv++; - var next; - while ((next = this.recvQ.shift()) !== undefined) { - this.session.write(next); - this.nrecv++; - } - if (this.recvQ.length == 0 && this.Qtimeout) { - clearTimeout(this.Qtimeout); - this.Qtimeout = null; - } - } - else if (oindex > 0 && oindex <= 1024) { - tslog("Client %s: Stashing message %d at %d", this.id, n, oindex - 1); - this.recvQ[oindex - 1] = data; - if (!this.Qtimeout) { - var nextn = this.nrecv + this.recvQ.length + 1; - this.Qtimeout = setTimeout(this.flushQ, 30000, this, nextn); - } - } - /* Otherwise, discard it */ - return; - }; - this.flushQ = function (client, n) { - /* Callback for when an unreceived message times out. - * n is the first empty space that will not be given up on. */ - if (!client.alive || client.nrecv >= n) - return; - client.nrecv++; - var next; - /* Clear the queue up to n */ - while (client.nrecv < n) { - next = client.recvQ.shift(); - if (next !== undefined) - client.session.write(next); - client.nrecv++; - } - /* Clear out anything that's ready. */ - while ((next = client.recvQ.shift()) !== undefined) { - client.session.write(next); - client.nrecv++; - } - /* Now set another timeout if necessary. */ - if (client.recvQ.length != 0) { - var nextn = client.nrecv + client.recvQ.length + 1; - client.Qtimeout = setTimeout(client.flushQ, 30000, client, nextn); - } - tslog("Flushing queue for player %s", player.id); - }; - this.reset = function () { - /* To be called when the game is taken over. */ - if (this.Qtimeout) { - clearTimeout(this.Qtimeout); - this.Qtimeout = null; - } - for (var i = 0; i < this.recvQ.length; i++) { - if (this.recvQ[i] !== undefined) { - this.session.write(this.recvQ[i]); - } - } - this.recvQ = []; - this.nrecv = 0; - this.nsend = 0; - this.sendQ = [{"t": "d", "n": this.nsend++, - "d": this.session.framebuf.toString("hex", 0, this.session.frameoff)}]; - }; - this.quit = function() { - if (this.alive) - this.session.close(); - }; - function openH(success, tag) { - if (success) { - ss.alive = true; - ss.session = sessions[tag]; - ss.h = sessions[tag].h; - ss.w = sessions[tag].w; - } - callback(ss, success); - } - function dataH(chunk) { - var reply = {}; - reply.t = "d"; - reply.n = ss.nsend++; - reply.d = chunk.toString("hex"); - ss.sendQ.push(reply); - } - function exitH() { - ss.alive = false; - ss.sendQ.push({"t": "q"}); - } - var handlers = {'open': openH, 'data': dataH, 'exit': exitH}; - this.session = new TermSession(gamename, lkey, dims, handlers); -} - // Also known as WebSocketAndTermSessionClosureGlueFactory function wsWatcher(conn, session) { var ss = this; // is this even needed? @@ -716,103 +543,6 @@ return; } -function startgame(req, res, formdata) { - if (!allowlogin) { - sendError(res, 6, null); - return; - } - if (!("key" in formdata)) { - sendError(res, 2, "No key given."); - return; - } - else if (!("game" in formdata)) { - sendError(res, 2, "No game specified."); - return; - } - var lkey = String(formdata["key"]); - if (!(lkey in logins)) { - sendError(res, 1, null); - return; - } - var username = logins[lkey].name; - var gname = formdata["game"]; - // If dims are not given or invalid, the constructor will handle it. - var dims = [formdata["h"], formdata["w"]]; - if (!(gname in games)) { - sendError(res, 2, "No such game: " + gname); - tslog("Request for nonexistant game \"%s\"", gname); - return; - } - // A callback to pass to the game-in-progress checker. - var launch = function(err, fname) { - var nodematch = new RegExp("^" + username + ":node:"); - if (fname && (fname.match(nodematch) === null)) { - /* It's being played in dgamelaunch. */ - sendError(res, 4, "dgamelaunch"); - tslog("%s is already playing %s", username, gname); - return; - } - // Game starting has been approved. - var respondlaunch = function(nclient, success) { - if (success) { - res.writeHead(200, {'Content-Type': 'application/json'}); - var reply = {"t": "s", "id": nclient.id, "w": nclient.w, "h": - nclient.h, "p": username, "g": gname}; - res.write(JSON.stringify(reply)); - res.end(); - } - else { - sendError(res, 5, "Failed to open TTY"); - tslog("Unable to allocate TTY for %s", gname); - } - }; - if (fname) { - for (var cid in clients) { - cli = clients[cid]; - if ((cli instanceof Player) && - cli.session.pname == username && - cli.session.game.uname == gname) { - cli.reset(); - respondlaunch(cli, true); - tslog("Game %d has been taken over.", cli.session.sessid); - return; - } - } - /* If there's no player, it's a WebSocket game, and shouldn't be - * seized. */ - sendError(res, 4, "WebSocket"); - } - else { - new Player(gname, lkey, dims, respondlaunch); - } - }; - checkprogress(username, games[gname], launch, []); -} - -function watch(req, res, formdata) { - if (!("g" in formdata) | !("p" in formdata)) { - sendError(res, 2, "Game or player not given"); - return; - } - if (!(formdata.g in games)) { - sendError(res, 2, "No such game: " + formdata.g); - return; - } - var tag = formdata.g = "/" + formdata.p; - if (!(tag in sessions)) { - sendError(res, 7); - return; - } - var session = sessions[tag]; - var watch = new Watcher(session); - var reply = {"t": "w", "w": session.w, "h": session.h, - "p": session.pname, "g": session.game.uname}; - res.writeHead(200, {'Content-Type': 'application/json'}); - res.write(JSON.stringify(reply)); - res.end(); - tslog("Game %d is being watched", tag); -} - /* Sets things up for a new user, like dgamelaunch's commands[register] */ function regsetup(username) { function regsetup_l2(err) { @@ -876,24 +606,7 @@ return; } -/* Ends the game, obviously. Less obviously, stops watching the game if - * the client is a Watcher instead of a Player. */ -function endgame(client, res) { - if (!client.alive) { - sendError(res, 7, null, true); - return; - } - client.quit(); - // Give things some time to happen. - if (client instanceof Player) - setTimeout(readFeed, 200, client, res); - else - readFeed(client, res); - return; -} - /* Stops a running game if the request has the proper key. */ -/* TODO does this still work? */ function stopgame(res, formdata) { if (!("key" in formdata) || !(formdata["key"] in logins)) { sendError(res, 1); @@ -940,19 +653,6 @@ checkprogress(pname, games[gname], checkback, []); } -/* TODO remove polling */ -function findClient(formdata, playersOnly) { - if (typeof(formdata) != "object") - return null; - if ("id" in formdata) { - var id = formdata["id"]; - if (id in clients && (!playersOnly || clients[id] instanceof Player)) { - return clients[id]; - } - } - return null; -} - function startProgressWatcher() { var watchdirs = []; for (var gname in games) { @@ -1032,22 +732,6 @@ return; } -/* TODO remove polling */ -function readFeed(client, res) { - if (!client) { - sendError(res, 7, null, true); - return; - } - var msgs = client.read(); - if (!allowlogin && !msgs.length) { - sendError(res, 6, null, true); - return; - } - res.writeHead(200, { "Content-Type": "application/json" }); - res.write(JSON.stringify(msgs)); - res.end(); -} - /* TODO simplify by storing timestamps instead of callin stat() */ function getStatus(callback) { var now = new Date(); @@ -1272,45 +956,12 @@ var target = url.parse(req.url).pathname; /* First figure out if the client is POSTing to a command interface. */ if (req.method == 'POST') { - if (target == '/feed') { - var client = findClient(formdata, false); - if (!client) { - sendError(res, 7, null, true); - return; - } - if (formdata.t == "q") { - /* The client wants to terminate the process. */ - endgame(client, res); - return; // endgame() calls readFeed() itself. - } - else if (formdata.t == "d" && typeof(formdata.d) == "string") { - if (!(client instanceof Player)) { - sendError(res, 7, "Watching", true); - return; - } - /* process the keys */ - var hexstr = formdata.d.replace(/[^0-9a-f]/gi, ""); - if (hexstr.length % 2 != 0) { - sendError(res, 2, "incomplete byte", true); - return; - } - var keybuf = new Buffer(hexstr, "hex"); - client.write(keybuf, formdata.n); - } - readFeed(client, res); - } - else if (target == "/login") { + if (target == "/login") { login(req, res, formdata); } else if (target == "/addacct") { register(req, res, formdata); } - else if (target == "/play") { - startgame(req, res, formdata); - } - else if (target == "/watch") { - watch(req, res, formdata); - } else if (target == "/quit") { stopgame(res, formdata); } @@ -1323,16 +974,7 @@ } } else if (req.method == 'GET' || req.method == 'HEAD') { - if (target == '/feed') { - if (req.method == 'HEAD') { - res.writeHead(200, {"Content-Type": "application/json"}); - res.end(); - } - else - sendError(res, 7, null, true); - return; - } - else if (target == '/status') { + if (target == '/status') { statusmsg(req, res); } else if (target.match(/^\/uinfo\//)) { @@ -1352,7 +994,6 @@ return; } req.on('end', respond); - } function wsHandler(wsRequest) { @@ -1403,6 +1044,7 @@ wsRequest.reject(404, "No such resource."); } +/* TODO use a list instead */ function pushStatus() { getStatus(function(info) { info["t"] = "t";