# HG changeset patch
# User John "Elwin" Edwards
# Date 1374504713 25200
# Node ID 789c094675f43464ecd2608f14450e4efef26a78
# Parent dcd07c1d846a3a99d368bc3809dd916ea3b4c29f
WebTTY: use WebSockets when possible.
diff -r dcd07c1d846a -r 789c094675f4 shterm.js
--- 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) {
diff -r dcd07c1d846a -r 789c094675f4 webtty.js
--- 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');