changeset 16:ef6127ed6da3

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.
author John "Elwin" Edwards <elwin@sdf.org>
date Thu, 17 May 2012 09:32:19 -0700
parents 7466927c17a5
children d3e3d6b4016b
files rlgterm.js rlgwebd.js
diffstat 2 files changed, 162 insertions(+), 75 deletions(-) [+]
line wrap: on
line diff
--- 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;
 }
 
--- 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);