# 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');