WebTTY: use WebSockets when possible.

This commit is contained in:
John "Elwin" Edwards 2013-07-22 07:51:53 -07:00
parent fabaea6849
commit d7df88f3cf
2 changed files with 183 additions and 18 deletions

View file

@ -6,6 +6,7 @@ var isalive = false; // Whether the session is currently active.
var nsend = 0; // The number of the next packet to send. var nsend = 0; // The number of the next packet to send.
var nrecv = 0; // The next packet expected. var nrecv = 0; // The next packet expected.
var msgQ = []; // Queue for out-of-order messages. var msgQ = []; // Queue for out-of-order messages.
var conn = null; // WebSocket
// A state machine that keeps track of polling the server. // A state machine that keeps track of polling the server.
var ajaxstate = { var ajaxstate = {
@ -202,15 +203,23 @@ function postResponseHandler() {
function sendback(str) { function sendback(str) {
/* For responding to terminal queries. */ /* For responding to terminal queries. */
if (conn) {
var msgObj = {"t": "d", "d": str};
conn.send(JSON.stringify(msgObj));
}
else {
var formdata = {"t": "d", "n": nsend++, "d": str}; var formdata = {"t": "d", "n": nsend++, "d": str};
var datareq = new XMLHttpRequest(); var datareq = new XMLHttpRequest();
datareq.onreadystatechange = postResponseHandler; datareq.onreadystatechange = postResponseHandler;
datareq.open('POST', '/feed', true); datareq.open('POST', '/feed', true);
datareq.send(JSON.stringify(formdata)); datareq.send(JSON.stringify(formdata));
}
return; return;
} }
function sendkey(ev) { function sendkey(ev) {
if (!isalive)
return;
var keynum = ev.keyCode; var keynum = ev.keyCode;
var code; var code;
if (keynum >= 65 && keynum <= 90) { if (keynum >= 65 && keynum <= 90) {
@ -245,13 +254,18 @@ function sendkey(ev) {
debug(1, "Ignoring keycode " + keynum); debug(1, "Ignoring keycode " + keynum);
return; return;
} }
if (isalive)
ev.preventDefault(); 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 formdata = {"t": "d", "n": nsend++, "d": code};
var datareq = new XMLHttpRequest(); var datareq = new XMLHttpRequest();
datareq.onreadystatechange = postResponseHandler; datareq.onreadystatechange = postResponseHandler;
datareq.open('POST', '/feed', true); datareq.open('POST', '/feed', true);
datareq.send(JSON.stringify(formdata)); datareq.send(JSON.stringify(formdata));
}
//dkey(code); //dkey(code);
return; return;
} }
@ -265,6 +279,8 @@ var kpkeys = { "KP1": "1b4f46", "KP2": "1b4f42", "KP3": "1b5b367e",
"KP7": "1b4f48", "KP8": "1b4f41", "KP9": "1b5b357e" }; "KP7": "1b4f48", "KP8": "1b4f41", "KP9": "1b5b357e" };
function vkey(c) { function vkey(c) {
if (!isalive)
return;
var keystr; var keystr;
if (c.match(/^[a-z]$/)) { if (c.match(/^[a-z]$/)) {
if (termemu.ctrlp()) { if (termemu.ctrlp()) {
@ -304,11 +320,17 @@ function vkey(c) {
else else
return; return;
//writeData("Sending " + keystr); //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 formdata = {"t": "d", "n": nsend++, "d": keystr};
var datareq = new XMLHttpRequest(); var datareq = new XMLHttpRequest();
datareq.onreadystatechange = postResponseHandler; datareq.onreadystatechange = postResponseHandler;
datareq.open('POST', '/feed', true); datareq.open('POST', '/feed', true);
datareq.send(JSON.stringify(formdata)); datareq.send(JSON.stringify(formdata));
}
return; return;
} }
@ -339,9 +361,45 @@ function togglectrl() {
return; 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) { function login(h, w) {
if (isalive) if (isalive)
return; return;
if (window.WebSocket) {
loginWS(h, w);
return;
}
params = {"login": true, "h": h, "w": w}; params = {"login": true, "h": h, "w": w};
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.onreadystatechange = function () { req.onreadystatechange = function () {
@ -368,6 +426,10 @@ function login(h, w) {
} }
function stop() { function stop() {
if (conn) {
conn.close();
return;
}
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.onreadystatechange = function () { req.onreadystatechange = function () {
if (req.readyState == 4 && req.status == 200) { if (req.readyState == 4 && req.status == 200) {

105
webtty.js
View file

@ -6,15 +6,83 @@ var url = require('url');
var path = require('path'); var path = require('path');
var fs = require('fs'); var fs = require('fs');
var pty = require(path.join(localModules, "pty.js")); 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 serveStaticRoot = fs.realpathSync(".");
var sessions = {}; var sessions = {};
var sessionsWS = {};
var env_dontuse = {"TMUX": true, "TMUX_PANE": true}; var env_dontuse = {"TMUX": true, "TMUX_PANE": true};
/* Constructor for TermSessions. Note that it opens the terminal and /* Constructor for TermSessions. Note that it opens the terminal and
* adds itself to the sessions dict. * 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) { function TermSession(sessid, h, w) {
/* Set up the sizes. */ /* Set up the sizes. */
w = Math.floor(Number(w)); w = Math.floor(Number(w));
@ -355,6 +423,41 @@ process.on("exit", function () {
return; 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"; 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/'); 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');