changeset 140:789c094675f4

WebTTY: use WebSockets when possible.
author John "Elwin" Edwards
date Mon, 22 Jul 2013 07:51:53 -0700
parents dcd07c1d846a
children 1a156a7746a7
files shterm.js webtty.js
diffstat 2 files changed, 183 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
--- a/shterm.js	Sat Jul 20 12:23:53 2013 -0700
+++ b/shterm.js	Mon Jul 22 07:51:53 2013 -0700
@@ -6,6 +6,7 @@
 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 = {
@@ -202,15 +203,23 @@
 
 function sendback(str) {
   /* For responding to terminal queries. */
-  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));
+  if (conn) {
+    var msgObj = {"t": "d", "d": str};
+    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));
+  }
   return;
 }
 
 function sendkey(ev) {
+  if (!isalive)
+    return;
   var keynum = ev.keyCode;
   var code;
   if (keynum >= 65 && keynum <= 90) {
@@ -245,13 +254,18 @@
     debug(1, "Ignoring keycode " + keynum);
     return;
   }
-  if (isalive)
-    ev.preventDefault();
-  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));
+  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));
+  }
   //dkey(code);
   return;
 }
@@ -265,6 +279,8 @@
                "KP7": "1b4f48", "KP8": "1b4f41", "KP9": "1b5b357e" };
 
 function vkey(c) {
+  if (!isalive)
+    return;
   var keystr;
   if (c.match(/^[a-z]$/)) {
     if (termemu.ctrlp()) {
@@ -304,11 +320,17 @@
   else
     return;
   //writeData("Sending " + keystr);
-  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));
+  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));
+  }
   return;
 }
 
@@ -339,9 +361,45 @@
   return;
 }
 
+function loginWS(h, w) {
+  if (conn)
+    return;
+  var sockurl = "ws://" + window.location.host + "/sock?w=" + w + "&h=" + h;
+  conn = new WebSocket(sockurl);
+  conn.onopen = function (event) {
+    isalive = true;
+    setTitle("Logged in");
+    debug(1, "Logged in via WebSocket");
+  }
+  conn.onmessage = function (event) {
+    var msgObj = JSON.parse(event.data);
+    if (msgObj.t == 'l') {
+      termemu.resize(msgObj.h, msgObj.w);
+    }
+    else if (msgObj.t == 'd') {
+      debug(0, msgObj.d);
+      writeData(msgObj.d);
+    }
+    else if (msgObj.t == 'q') {
+      debug(0, "Quit message!");
+      conn.close();
+    }
+  }
+  conn.onclose = function (event) {
+    conn = null;
+    isalive = false;
+    debug(1, "WebSocket connection closed.");
+    setTitle("Not connected.");
+  }
+}
+
 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 () {
@@ -368,6 +426,10 @@
 }
 
 function stop() {
+  if (conn) {
+    conn.close();
+    return;
+  }
   var req = new XMLHttpRequest();
   req.onreadystatechange = function () {
     if (req.readyState == 4 && req.status == 200) {
--- a/webtty.js	Sat Jul 20 12:23:53 2013 -0700
+++ b/webtty.js	Mon Jul 22 07:51:53 2013 -0700
@@ -6,15 +6,83 @@
 var path = require('path');
 var fs = require('fs');
 var pty = require(path.join(localModules, "pty.js"));
+var child_process = require("child_process");
+var webSocketServer = require(path.join(localModules, "websocket")).server;
 
 var serveStaticRoot = fs.realpathSync(".");
 var sessions = {};
+var sessionsWS = {};
 
 var env_dontuse = {"TMUX": true, "TMUX_PANE": true};
 
 /* Constructor for TermSessions.  Note that it opens the terminal and 
  * adds itself to the sessions dict. 
  */
+function TermSessionWS(conn, h, w) {
+  var ss = this;
+  /* 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;
+  this.conn = conn;
+  /* 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);
+  this.alive = true;
+  this.term.on("data", function (datastr) {
+    var buf = new Buffer(datastr);
+    if (ss.conn.connected)
+      ss.conn.sendUTF(JSON.stringify({"t": "d", "d": buf.toString("hex")}));
+  });
+  this.term.on("exit", function () {
+    ss.alive = false;
+    /* Wait for all the data to get collected */
+    setTimeout(ss.cleanup, 1000);
+  });
+  this.conn.on("message", function (msg) {
+    try {
+      var msgObj = JSON.parse(msg.utf8Data);
+    }
+    catch (e) {
+      return;
+    }
+    if (msgObj.t == "d") {
+      var hexstr = msgObj["d"].replace(/[^0-9a-f]/gi, "");
+      if (hexstr.length % 2 != 0) {
+        return;
+      }
+      var keybuf = new Buffer(hexstr, "hex");
+      ss.term.write(keybuf);
+    }
+  });
+  this.conn.on("close", function (msg) {
+    if (ss.alive)
+      ss.term.kill('SIGHUP');
+    console.log("WebSocket connection closed.");
+  });
+  this.cleanup = function () {
+    /* Call this when the child is dead. */
+    if (ss.alive)
+      return;
+    if (ss.conn.connected) {
+      ss.conn.sendUTF(JSON.stringify({"t": "q"}));
+    }
+  };
+  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));
@@ -355,6 +423,41 @@
   return;
 });
 
+function wsRespond(req) {
+  var w, h, conn;
+  if (req.resourceURL.pathname == "/sock") {
+    w = parseInt(req.resourceURL.query.w);
+    if (isNaN(w) || w <= 0 || w > 256)
+      w = 80;
+    h = parseInt(req.resourceURL.query.h);
+    if (isNaN(h) || h <= 0 || h > 256)
+      h = 25;
+    conn = req.accept(null, req.origin);
+    new TermSessionWS(conn, h, w);
+  }
+  else {
+    req.reject(404, "No such resource.");
+  }
+}
+
+/* The pty.js module doesn't wait for the processes it spawns, so they 
+ * become zombies, which leads to unpleasantness when the system runs
+ * out of process table entries.  But if the child_process module is 
+ * initialized and a child spawned, node will continue waiting for any
+ * children.
+ * Someday, some developer will get the bright idea of tracking how many
+ * processes the child_process module has spawned, and not waiting if 
+ * it's zero.  Until then, the following useless line will protect us 
+ * from the zombie hordes.
+ * Figuring this out was almost as interesting as the Rogue bug where 
+ * printf debugging altered whether the high score list was checked.
+ */
+child_process.spawn("/bin/true");
+
 process.env["TERM"] = "xterm-256color";
-http.createServer(handler).listen(8080, "127.0.0.1");
+var webServer = http.createServer(handler);
+webServer.listen(8080, "127.0.0.1");
 console.log('Server running at http://127.0.0.1:8080/'); 
+var wsServer = new webSocketServer({"httpServer": webServer});
+wsServer.on("request", wsRespond);
+console.log('WebSockets online');