changeset 153:c4a32007d2dc

WebTTY: remove polling. Communication now uses WebSockets only.
author John "Elwin" Edwards
date Mon, 27 Jan 2014 16:02:27 -0800
parents 1d3cfe10974a
children b5a1430d0f71
files shterm.js webtty.js
diffstat 2 files changed, 19 insertions(+), 390 deletions(-) [+]
line wrap: on
line diff
--- a/shterm.js	Sun Jan 26 19:56:02 2014 -0800
+++ b/shterm.js	Mon Jan 27 16:02:27 2014 -0800
@@ -3,53 +3,8 @@
  */
 
 var isalive = false; // Whether the session is currently active.
-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.
 var conn = null; // WebSocket
 
-// 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(100);
-    this.state = 0;
-  },
-  gotnothing: function () {
-    if (this.state == 0) {
-      this.set(100);
-      this.state = 1;
-    }
-    else if (this.state == 1) {
-      this.set(300);
-      this.state = 2;
-    }
-    else if (this.state == 2) {
-      this.set(1000);
-      this.state = 3;
-    }
-    else {
-      this.set(5000);
-      this.state = 3;
-    }
-  },
-  posted: function () {
-    this.set(100);
-    this.state = 0;
-  }
-};
-
 function writeData(hexstr) {
   var codenum;
   var codes = [];
@@ -119,88 +74,6 @@
   return;
 }
 
-function processMsg(response) {
-  if (response.t != "d" || typeof(response.d) != "string")
-    return;
-  if (response.n === nrecv) {
-    writeData(response.d);
-    nrecv++;
-    var next;
-    /* msgQ must be shifted every time nrecv is incremented, but the process
-     * stops whenever an empty space, corresponding to an unarrived message,
-     * is encountered.
-     */
-    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 " + response.n + ", expected " + nrecv);
-    msgQ[response.n - nrecv - 1] = response;
-  }
-  else {
-    /* This message's number was encountered previously. */
-    debug(1, "Discarding packet " + response.n + ", expected " + nrecv);
-  }
-}
-
-function getData() {
-  if (!isalive)
-    return;
-  var datareq = new XMLHttpRequest();
-  datareq.onreadystatechange = function () {
-    if (datareq.readyState == 4 && datareq.status == 200) {
-      var response = JSON.parse(this.responseText);
-      if (!response.t)
-        return;
-      else if (response.t == "E") {
-        if (response.c == 1) {
-          isalive = false;
-          debug(1, "Server error: " + response.s);
-        }
-      }
-      else if (response.t == "n")
-        ajaxstate.gotnothing();
-      else if (response.t == "d") {
-        processMsg(response);
-        ajaxstate.gotdata();
-      }
-      return;
-    }
-  };
-  datareq.open('GET', '/feed', true);
-  datareq.send(null);
-  return;
-}
-
-function postResponseHandler() {
-  if (this.readyState == 4 && this.status == 200) {
-    var response = JSON.parse(this.responseText);
-    if (!response.t || response.t == "n")
-      return;
-    else if (response.t == "E") {
-      if (response.c == 1) {
-        isalive = false;
-        debug(1, "Server error: " + response.s);
-      }
-      return;
-    }
-    else if (response.t != "d")
-      return;
-    /* It is a data message */
-    if (response.d) {
-      processMsg(response);
-      //debug(1, "Got packet " + response.n);
-    }
-    ajaxstate.posted();
-    return;
-  }
-}
-
 function sendback(str) {
   /* For responding to terminal queries. */
   if (conn) {
@@ -208,11 +81,7 @@
     conn.send(JSON.stringify(msgObj));
   }
   else {
-    var formdata = {"t": "d", "n": nsend++, "d": str};
-    var datareq = new XMLHttpRequest();
-    datareq.onreadystatechange = postResponseHandler;
-    datareq.open('POST', '/feed', true);
-    datareq.send(JSON.stringify(formdata));
+    debug(1, "No connection: cannot send " + str);
   }
   return;
 }
@@ -255,17 +124,8 @@
     return;
   }
   ev.preventDefault();
-  if (conn) {
-    var msgObj = {"t": "d", "d": code};
-    conn.send(JSON.stringify(msgObj));
-  }
-  else {
-    var formdata = {"t": "d", "n": nsend++, "d": code};
-    var datareq = new XMLHttpRequest();
-    datareq.onreadystatechange = postResponseHandler;
-    datareq.open('POST', '/feed', true);
-    datareq.send(JSON.stringify(formdata));
-  }
+  var msgObj = {"t": "d", "d": code};
+  conn.send(JSON.stringify(msgObj));
   //dkey(code);
   return;
 }
@@ -320,17 +180,8 @@
   else
     return;
   //writeData("Sending " + keystr);
-  if (conn) {
-    var msgObj = {"t": "d", "d": keystr};
-    conn.send(JSON.stringify(msgObj));
-  }
-  else {
-    var formdata = {"t": "d", "n": nsend++, "d": keystr};
-    var datareq = new XMLHttpRequest();
-    datareq.onreadystatechange = postResponseHandler;
-    datareq.open('POST', '/feed', true);
-    datareq.send(JSON.stringify(formdata));
-  }
+  var msgObj = {"t": "d", "d": keystr};
+  conn.send(JSON.stringify(msgObj));
   return;
 }
 
@@ -361,9 +212,13 @@
   return;
 }
 
-function loginWS(h, w) {
+function login(h, w) {
   if (conn)
     return;
+  if (!(window.WebSocket)) {
+    debug(1, "Cannot connect: WebSockets not supported");
+    return;
+  }
   var sockurl = "ws://" + window.location.host + "/sock?w=" + w + "&h=" + h;
   conn = new WebSocket(sockurl);
   conn.onopen = function (event) {
@@ -393,54 +248,13 @@
   }
 }
 
-function login(h, w) {
-  if (isalive)
-    return;
-  if (window.WebSocket) {
-    loginWS(h, w);
-    return;
-  }
-  params = {"login": true, "h": h, "w": w};
-  var req = new XMLHttpRequest();
-  req.onreadystatechange = function () {
-    if (req.readyState == 4 && req.status == 200) {
-      var logindict = JSON.parse(req.responseText);
-      if (logindict.login) {
-        /* Success */
-        termemu.resize(logindict.h, logindict.w);
-        isalive = true;
-        nsend = 0;
-        nrecv = 0;
-	setTitle("Logged in");
-        debug(1, "Logged in with id " + logindict.id);
-        getData();
-        return;
-      }
-      return;
-    }
-  };
-  req.open('POST', '/login', true);
-  req.send(JSON.stringify(params));
-  //req.send("login=login&h=" + String(h) + "&w=" + String(w));
-  return;
-}
-
 function stop() {
   if (conn) {
     conn.close();
-    return;
   }
-  var req = new XMLHttpRequest();
-  req.onreadystatechange = function () {
-    if (req.readyState == 4 && req.status == 200) {
-      /* Figure out whether or not it worked. */
-      /* FIXME the server might respond with output. */
-      isalive = false;
-      return;
-    }
-  };
-  req.open('POST', '/feed', true);
-  req.send(JSON.stringify({"t": "q", "n": nsend++}));
+  else {
+    debug(1, "Cannot stop: connection already closed");
+  }
   return;
 }
 
--- a/webtty.js	Sun Jan 26 19:56:02 2014 -0800
+++ b/webtty.js	Mon Jan 27 16:02:27 2014 -0800
@@ -11,7 +11,7 @@
 
 var serveStaticRoot = fs.realpathSync(".");
 var sessions = {};
-var sessionsWS = {};
+var nsessid = 0;
 
 var env_dontuse = {"TMUX": true, "TMUX_PANE": true};
 
@@ -79,94 +79,11 @@
       ss.conn.sendUTF(JSON.stringify({"t": "q"}));
     }
   };
+  sessions[nsessid++] = this;
   this.conn.sendUTF(JSON.stringify({"t": "l", "w": w, "h": h}));
   console.log("New WebSocket connection.");
 }
 
-function TermSession(sessid, h, w) {
-  /* Set up the sizes. */
-  w = Math.floor(Number(w));
-  if (!(w > 0 && w < 256))
-    w = 80;
-  this.w = w;
-  h = Math.floor(Number(h));
-  if (!(h > 0 && h < 256))
-    h = 25;
-  this.h = h;
-  /* Customize the environment. */
-  var childenv = {};
-  for (var key in process.env) {
-    if (!(key in env_dontuse))
-      childenv[key] = process.env[key];
-  }
-  var spawnopts = {"env": childenv, "cwd": process.env["HOME"], 
-                   "rows": this.h, "cols": this.w};
-  this.term = pty.spawn("bash", [], spawnopts);
-  var ss = this;
-  /* Eventually we'll need to make sure the sessid isn't in use yet. */
-  this.sessid = sessid;
-  this.alive = true;
-  this.data = []; // Buffer for the process' output.
-  this.nsend = 0; // Number to use for the next message sent.
-  this.nrecv = 0; // Number expected on the next message received.
-  this.msgQ = []; // Queue for messages that arrived out of order.
-  this.term.on("data", function (buf) {
-    ss.data.push(buf);
-  });
-  this.term.on("exit", function () {
-    ss.alive = false;
-    /* Wait for all the data to get collected */
-    setTimeout(ss.cleanup, 1000);
-  });
-  this.write = function (data, n) {
-    if (!this.alive) {
-      /* Throw some kind of exception? */
-      return;
-    }
-    if (n !== this.nrecv) {
-      console.log("Session " + this.sessid + ": Expected message " + this.nrecv + ", got " + n);
-    }
-    this.nrecv = n + 1;
-    this.term.write(data);
-  };
-  this.read = function () {
-    if (this.data.length == 0)
-      return null;
-    var pos = 0;
-    var i = 0;
-    for (i = 0; i < this.data.length; i++)
-      pos += Buffer.byteLength(this.data[i]);
-    var nbuf = new Buffer(pos);
-    var tptr;
-    pos = 0;
-    while (this.data.length > 0) {
-      tptr = new Buffer(this.data.shift());
-      tptr.copy(nbuf, pos);
-      pos += tptr.length;
-    }
-    return nbuf;
-  };
-  this.close = function () {
-    if (this.alive)
-      this.term.kill('SIGHUP');
-  };
-  this.cleanup = function () {
-    /* Call this when the child is dead. */
-    if (this.alive)
-      return;
-    /* Give the client a chance to read any leftover data. */
-    if (ss.data.length > 0)
-      setTimeout(ss.remove, 8000);
-    else
-      ss.remove();
-  };
-  this.remove = function () {
-    delete sessions[ss.sessid];
-    console.log("Session " + this.sessid + " removed.");
-  };
-  sessions[sessid] = this;
-}
-
 function randkey() {
   rnum = Math.floor(Math.random() * 65536 * 65536);
   hexstr = rnum.toString(16);
@@ -226,45 +143,6 @@
   return jsonobj;
 }
 
-function login(req, res, formdata) {
-  var resheaders = {'Content-Type': 'text/plain'};
-  var sessid = randkey();
-  /* The TermSession constructor will check these thoroughly too, but 
-   * you can't be too suspicious of client-supplied data. */
-  var w = 80;
-  var h = 25;
-  var t;
-  if ("w" in formdata) {
-    t = Math.floor(Number(formdata["w"]));
-    if (t > 0 && t < 256)
-      w = t;
-  }
-  if ("h" in formdata) {
-    t = Math.floor(Number(formdata["h"]));
-    if (t > 0 && t < 256)
-      h = t;
-  }
-  var nsession = new TermSession(sessid, h, w);
-  resheaders["Set-Cookie"] = "ID=" + sessid;
-  res.writeHead(200, resheaders);
-  var logindict = {"login": true, "id": sessid, "w": w, "h": h};
-  res.write(JSON.stringify(logindict));
-  res.end();
-  console.log("Started new session with key " + sessid + ", pid " + nsession.term.pid);
-  return;
-}
-
-function findTermSession(req) {
-  var cookies = getCookies(req);
-  if ("id" in cookies) {
-    var sessid = cookies["id"];
-    if (sessid in sessions) {
-      return sessions[sessid];
-    }
-  }
-  return null;
-}
-
 function serveStatic(req, res, fname) {
   var nname = path.normalize(fname);
   if (nname == "" || nname == "/")
@@ -306,27 +184,6 @@
   return;
 }
 
-function readFeed(res, term) {
-  res.writeHead(200, { "Content-Type": "text/plain" });
-  if (term) {
-    var answer = {};
-    var result = term.read();
-    if (result == null) {
-      answer["t"] = "n";
-    }
-    else {
-      answer["t"] = "d";
-      answer["d"] = result.toString("hex");
-      answer["n"] = term.nsend++;
-    }
-    res.write(JSON.stringify(answer));
-    res.end();
-  }
-  else {
-    sendError(res, 1);
-  }
-}
-
 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data" ];
 
 function sendError(res, ecode) {
@@ -353,55 +210,14 @@
   /* This will send the response once the whole request is here. */
   function respond() {
     var target = url.parse(req.url).pathname;
-    var cterm = findTermSession(req);
-    /* First figure out if the client is POSTing to a command interface. */
+    /* Currently only static files and WebSockets are needed. */
     if (req.method == 'POST') {
       formdata = getFormValues(reqbody);
-      if (target == '/feed') {
-        if (!cterm) {
-          sendError(res, 1);
-          return;
-        }
-        if (formdata["t"] == "q") {
-          /* The client wants to quit. */
-          // FIXME need to send a message back to the client
-          cterm.close();
-        }
-        else if (formdata["t"] == "d" && typeof(formdata["d"]) == "string") {
-          /* process the keys */
-          hexstr = formdata["d"].replace(/[^0-9a-f]/gi, "");
-          if (hexstr.length % 2 != 0) {
-            sendError(res, 2);
-            return;
-          }
-          keybuf = new Buffer(hexstr, "hex");
-          cterm.write(keybuf, formdata["n"]);
-        }
-        readFeed(res, cterm);
-      }
-      else if (target == "/login") {
-        login(req, res, formdata);
-      }
-      else {
-        res.writeHead(405, resheaders);
-        res.end();
-      }
+      res.writeHead(405, resheaders);
+      res.end();
     }
     else if (req.method == 'GET' || req.method == 'HEAD') {
-      if (target == '/feed') {
-        if (!cterm) {
-          sendError(res, 1);
-          return;
-        }
-        readFeed(res, cterm);
-      }
-      /* Default page, create a new term */
-      /* FIXME New term not created anymore, is a special case still needed? */
-      else if (target == '/') {
-        serveStatic(req, res, "/");
-      }
-      else /* Go look for it in the filesystem */
-        serveStatic(req, res, target);
+      serveStatic(req, res, target);
     }
     else { /* Some other method */
       res.writeHead(501, resheaders);
@@ -411,7 +227,6 @@
     return;
   }
   req.on('end', respond);
-
 }
 
 process.on("exit", function () {