# HG changeset patch # User John "Elwin" Edwards # Date 1337272339 25200 # Node ID ef6127ed6da34171ada52d4f9bd1ef990a0e01d7 # Parent 7466927c17a53a2aeb746f9652785910a644ec94 RLGWeb: switch to JSON protocol. Port the JSON communication from WebTTY to RLGWeb. Fixing out-of-order messages is still not implemented on the server side. Terminal size is still hard-coded. Unused code is still lying around. diff -r 7466927c17a5 -r ef6127ed6da3 rlgterm.js --- a/rlgterm.js Tue May 15 16:26:28 2012 -0700 +++ b/rlgterm.js Thu May 17 09:32:19 2012 -0700 @@ -111,33 +111,67 @@ 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. */ + * 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 msglines = msg.split("\n"); - var havedata = null; - if (!msglines[0]) + var msgDict; + var havedata = null; // eventual return value + try { + msgDict = JSON.parse(msg); + } catch (e) { + if (e instanceof SyntaxError) + return null; + } + if (!msgDict.t) return null; - if (msglines[0].charAt(0) == 'd') { - if (msglines[1]){ - writeData(msglines[1]); - havedata = true; + else if (msgDict.t == "E") { + if (msgDict.c == 1) { + logout(); + } + debug(1, "Server error: " + msgDict.s); + } + else if (msgDict.t == "n") { + havedata = false; + } + // A data message + else if (msgDict.t == "d"){ + if (msgDict.n === nrecv) { + writeData(msgDict.d); + nrecv++; + /* Process anything in the queue that's now ready. */ + var next; + while ((next = msgQ.shift()) !== undefined) { + writeData(next.d); + nrecv++; + } + } + else if (response.n > nrecv) { + /* The current message comes after one still missing. Queue this one + * for later use. */ + debug(1, "Got packet " + msgDict.n + ", expected " + nrecv); + msgQ[msgDict.n - nrecv - 1] = msgDict; } else { - havedata = false; + /* This message's number was encountered previously. */ + debug(1, "Discarding packet " + msgDict.n + ", expected " + nrecv); } + havedata = true; } - else if (msglines[0] == "E1") { - logout(); + else if (msgDict.t == "T") { + setTitle(msgDict.d); } - else if (msglines[0].charAt(0) == "T") { - setTitle(msglines[1]); - } - else if (msglines[0] == "q1") { + else if (msgDict.t == "q") { logout(); } else { - debug(1, "Unrecognized server message " + msglines[0]); + debug(1, "Unrecognized server message " + msg); } return havedata; } @@ -146,6 +180,7 @@ if (termemu.sessid == null) return; var datareq = new XMLHttpRequest(); + var msg = JSON.stringify({"id": termemu.sessid, "t": "n"}); datareq.onreadystatechange = function () { if (datareq.readyState == 4 && datareq.status == 200) { var wasdata = processMsg(datareq.responseText); @@ -159,7 +194,7 @@ } }; datareq.open('POST', '/feed', true); - datareq.send("id=" + termemu.sessid); + datareq.send(msg); return; } @@ -174,10 +209,11 @@ function sendback(str) { /* For responding to terminal queries. */ + var msgDict = {"id": termemu.sessid, "t": "d", "n": nsend++, "d": str}; var datareq = new XMLHttpRequest(); datareq.onreadystatechange = postResponseHandler; datareq.open('POST', '/feed', true); - datareq.send("id=" + termemu.sessid + "&keys=" + str); + datareq.send(JSON.stringify(msgDict)); return; } @@ -219,12 +255,14 @@ debug(1, "Ignoring keycode " + keynum); return; } + // Isn't this check redundant? if (termemu.sessid != null) ev.preventDefault(); var datareq = new XMLHttpRequest(); + var msgDict = {"id": termemu.sessid, "t": "d", "n": nsend++, "d": code}; datareq.onreadystatechange = postResponseHandler; datareq.open('POST', '/feed', true); - datareq.send("id=" + termemu.sessid + "&keys=" + code); + datareq.send(JSON.stringify(msgDict)); return; } @@ -270,11 +308,11 @@ } else return; - //writeData("Sending " + keystr); var datareq = new XMLHttpRequest(); + var msgDict = {"id": termemu.sessid, "t": "d", "n": nsend++, "d": keystr}; datareq.onreadystatechange = postResponseHandler; datareq.open('POST', '/feed', true); - datareq.send("id=" + termemu.sessid + "&keys=" + keystr); + datareq.send(JSON.stringify(msgDict)); return; } @@ -309,31 +347,34 @@ ev.preventDefault(); if (termemu.sessid != null) return; - var formname = document.getElementById("input_name").value; - var formpass = document.getElementById("input_pw").value; - var formgame = document.getElementById("input_game").value; - var formdata = "game=" + encodeURIComponent(formgame) + "&name=" + encodeURIComponent(formname) + "&pw=" + encodeURIComponent(formpass); + var loginmsg = {}; + loginmsg["name"] = document.getElementById("input_name").value; + loginmsg["pw"] = document.getElementById("input_pw").value; + loginmsg["game"] = document.getElementById("input_game").value; + loginmsg["h"] = 24; + loginmsg["w"] = 80; var req = new XMLHttpRequest(); req.onreadystatechange = function () { - if (req.readyState == 4 && req.status == 200) { - var datalines = req.responseText.split("\n"); - if (datalines[0] == 'l1') { - /* Success */ - termemu.sessid = datalines[1]; - setTitle("Playing as " + formname); - debug(1, "Logged in with id " + termemu.sessid); - document.getElementById("loginform").style.display = "none"; - getData(); - } - else { - debug(1, "Could not start game: " + datalines[1]); - document.getElementById("input_name").value = ""; - document.getElementById("input_pw").value = ""; - } + if (req.readyState != 4 || req.status != 200) + return; + var reply = JSON.parse(req.responseText); + if (reply.t == 'l') { + /* Success */ + termemu.sessid = reply.id; + termemu.resize(reply.h, reply.w); + setTitle("Playing as " + loginmsg["name"]); + debug(1, "Logged in with id " + termemu.sessid); + document.getElementById("loginform").style.display = "none"; + getData(); + } + else if (reply.t == 'E') { + debug(1, "Could not start game: " + reply.s); + document.getElementById("input_name").value = ""; + document.getElementById("input_pw").value = ""; } }; req.open('POST', '/login', true); - req.send(formdata); + req.send(JSON.stringify(loginmsg)); return; } @@ -342,6 +383,9 @@ return; termemu.sessid = null; setTitle("Game over."); + nsend = 0; + nrecv = 0; + msgQ = []; document.getElementById("loginform").style.display = "block"; return; } @@ -355,7 +399,7 @@ } }; req.open('POST', '/feed', true); - req.send("id=" + termemu.sessid + "&quit=quit"); + req.send(JSON.stringify({"id": termemu.sessid, "t": "q"})); return; } diff -r 7466927c17a5 -r ef6127ed6da3 rlgwebd.js --- a/rlgwebd.js Tue May 15 16:26:28 2012 -0700 +++ b/rlgwebd.js Thu May 17 09:32:19 2012 -0700 @@ -43,7 +43,7 @@ * adds itself to the sessions dict. It currently assumes the user has * been authenticated. */ -function TermSession(game, user, files) { +function TermSession(game, user, files, dims) { /* First make sure starting the game will work. */ if (!(game in games)) { // TODO: throw an exception instead @@ -57,15 +57,33 @@ } /* Grab a spot in the sessions table. */ sessions[this.sessid] = this; + /* State for messaging. */ + this.nsend = 0; + this.nrecv = 0; + this.msgQ = [] + /* Set up the sizes. */ + this.w = Math.floor(Number(dims[1])); + if (!(this.w > 0 && this.w < 256)) + this.w = 80; + this.h = Math.floor(Number(dims[0])); + if (!(this.h > 0 && this.h < 256)) + this.h = 24; + /* Environment. */ + var childenv = {}; + for (var key in process.env) { + childenv[key] = process.env[key]; + } + childenv["PTYHELPER"] = String(this.h) + "x" + String(this.w); /* TODO handle tty-opening errors */ /* TODO make argument-finding into a method */ args = [games[game].path, "-n", user.toString()]; - this.child = child_process.spawn("/bin/ptyhelper", args); + this.child = child_process.spawn("/bin/ptyhelper", args, {"env": childenv}); var ss = this; this.alive = true; this.data = []; this.lock = files[0]; - fs.writeFile(this.lock, this.child.pid.toString() + '\n80\n24\n', "utf8"); + fs.writeFile(this.lock, this.child.pid.toString() + '\n' + this.w + '\n' + + this.h + '\n', "utf8"); this.record = fs.createWriteStream(files[1], { mode: 0664 }); /* END setup */ function ttyrec_chunk(buf) { @@ -206,6 +224,22 @@ return data; } +function getMsg(posttext) { + var jsonobj; + if (!posttext) + return {}; + try { + jsonobj = JSON.parse(posttext); + } + catch (e) { + if (e instanceof SyntaxError) + return {}; + } + if (typeof(jsonobj) != "object") + return {}; + return jsonobj; +} + function auth(username, password) { // Real authentication not implemented return true; @@ -224,9 +258,10 @@ sendError(res, 2, "Password not given."); return; } - var username = formdata["name"][0]; - var password = formdata["pw"][0]; - var gname = formdata["game"][0]; + var username = formdata["name"]; + var password = formdata["pw"]; + var gname = formdata["game"]; + var dims = [formdata["h"], formdata["w"]]; if (!(gname in games)) { sendError(res, 2, "No such game: " + gname); console.log("Request for nonexistant game \"" + gname + "\""); @@ -239,13 +274,15 @@ var ts = timestamp(); var lockfile = path.join(progressdir, username + ":node:" + ts + ".ttyrec"); var ttyrec = path.join("/dgldir/ttyrec", username, gname, ts + ".ttyrec"); - var nsession = new TermSession(gname, username, [lockfile, ttyrec]); + var nsession = new TermSession(gname, username, [lockfile, ttyrec], dims); if (nsession) { /* Technically there's a race condition for the "lock"file, but since * it requires the user deliberately starting two games at similar times, * it's not too serious. We can't get O_EXCL in Node anyway. */ res.writeHead(200, {'Content-Type': 'text/plain'}); - res.write("l1\n" + nsession.sessid + "\n"); + var reply = {"t": "l", "id": nsession.sessid, "w": nsession.w, "h": + nsession.h}; + res.write(JSON.stringify(reply)); res.end(); console.log("%s playing %s (key %s, pid %d)", username, gname, nsession.sessid, nsession.child.pid); @@ -309,14 +346,16 @@ cterm.close(); var resheaders = {'Content-Type': 'text/plain'}; res.writeHead(200, resheaders); - res.write("q1\n\n"); + res.write(JSON.stringify({"t": "q"})); res.end(); return; } function findTermSession(formdata) { + if (typeof(formdata) != "object") + return null; if ("id" in formdata) { - var sessid = formdata["id"][0]; + var sessid = formdata["id"]; if (sessid in sessions) { return sessions[sessid]; } @@ -370,22 +409,24 @@ function readFeed(res, term) { if (term) { + var reply = {}; var result = term.read(); + if (result == null) { + if (term.alive) + reply.t = "n"; + else + reply.t = "q"; + } + else { + reply.t = "d"; + reply.n = term.nsend++; + reply.d = result.toString("hex"); + } res.writeHead(200, { "Content-Type": "text/plain" }); - if (result == null) - resultstr = ""; - else - resultstr = result.toString("hex"); - if (result == null && !term.alive) { - /* Child has terminated and data is flushed. */ - res.write("q1\n\n"); - } - else - res.write("d" + resultstr.length.toString() + "\n" + resultstr + "\n"); + res.write(JSON.stringify(reply)); res.end(); } else { - //console.log("Where's the term?"); sendError(res, 1, null); } } @@ -395,14 +436,14 @@ function sendError(res, ecode, msg) { res.writeHead(200, { "Content-Type": "text/plain" }); - if (ecode < errorcodes.length && ecode > 0) { - var emsg = errorcodes[ecode]; - if (msg) - emsg += ": " + msg; - res.write("E" + ecode + '\n' + emsg + '\n'); - } - else - res.write("E0\nGeneric Error\n"); + var edict = {"t": "E"}; + if (!(ecode < errorcodes.length && ecode > 0)) + ecode = 0; + edict["c"] = ecode; + edict["s"] = errorcodes[ecode]; + if (msg) + edict["s"] += ": " + msg; + res.write(JSON.stringify(edict)); res.end(); } @@ -421,7 +462,8 @@ /* This will send the response once the whole request is here. */ function respond() { - formdata = getFormValues(reqbody); + //formdata = getFormValues(reqbody); + formdata = getMsg(reqbody); var target = url.parse(req.url).pathname; var cterm = findTermSession(formdata); /* First figure out if the client is POSTing to a command interface. */ @@ -431,18 +473,19 @@ sendError(res, 1, null); return; } - if ("quit" in formdata) { + if (formdata.t == "q") { /* The client wants to terminate the process. */ logout(cterm, res); } - else if (formdata["keys"]) { + else if (formdata.t == "d" && typeof(formdata.d) == "string") { /* process the keys */ - hexstr = formdata["keys"][0].replace(/[^0-9a-f]/gi, ""); + hexstr = formdata.d.replace(/[^0-9a-f]/gi, ""); if (hexstr.length % 2 != 0) { sendError(res, 2, "incomplete byte"); return; } keybuf = new Buffer(hexstr, "hex"); + /* TODO OoO correction */ cterm.write(keybuf); } readFeed(res, cterm);