comparison webtty.js @ 153:c4a32007d2dc

WebTTY: remove polling. Communication now uses WebSockets only.
author John "Elwin" Edwards
date Mon, 27 Jan 2014 16:02:27 -0800
parents 789c094675f4
children 5372f1f97cf5
comparison
equal deleted inserted replaced
150:1d3cfe10974a 153:c4a32007d2dc
9 var child_process = require("child_process"); 9 var child_process = require("child_process");
10 var webSocketServer = require(path.join(localModules, "websocket")).server; 10 var webSocketServer = require(path.join(localModules, "websocket")).server;
11 11
12 var serveStaticRoot = fs.realpathSync("."); 12 var serveStaticRoot = fs.realpathSync(".");
13 var sessions = {}; 13 var sessions = {};
14 var sessionsWS = {}; 14 var nsessid = 0;
15 15
16 var env_dontuse = {"TMUX": true, "TMUX_PANE": true}; 16 var env_dontuse = {"TMUX": true, "TMUX_PANE": true};
17 17
18 /* Constructor for TermSessions. Note that it opens the terminal and 18 /* Constructor for TermSessions. Note that it opens the terminal and
19 * adds itself to the sessions dict. 19 * adds itself to the sessions dict.
77 return; 77 return;
78 if (ss.conn.connected) { 78 if (ss.conn.connected) {
79 ss.conn.sendUTF(JSON.stringify({"t": "q"})); 79 ss.conn.sendUTF(JSON.stringify({"t": "q"}));
80 } 80 }
81 }; 81 };
82 sessions[nsessid++] = this;
82 this.conn.sendUTF(JSON.stringify({"t": "l", "w": w, "h": h})); 83 this.conn.sendUTF(JSON.stringify({"t": "l", "w": w, "h": h}));
83 console.log("New WebSocket connection."); 84 console.log("New WebSocket connection.");
84 }
85
86 function TermSession(sessid, h, w) {
87 /* Set up the sizes. */
88 w = Math.floor(Number(w));
89 if (!(w > 0 && w < 256))
90 w = 80;
91 this.w = w;
92 h = Math.floor(Number(h));
93 if (!(h > 0 && h < 256))
94 h = 25;
95 this.h = h;
96 /* Customize the environment. */
97 var childenv = {};
98 for (var key in process.env) {
99 if (!(key in env_dontuse))
100 childenv[key] = process.env[key];
101 }
102 var spawnopts = {"env": childenv, "cwd": process.env["HOME"],
103 "rows": this.h, "cols": this.w};
104 this.term = pty.spawn("bash", [], spawnopts);
105 var ss = this;
106 /* Eventually we'll need to make sure the sessid isn't in use yet. */
107 this.sessid = sessid;
108 this.alive = true;
109 this.data = []; // Buffer for the process' output.
110 this.nsend = 0; // Number to use for the next message sent.
111 this.nrecv = 0; // Number expected on the next message received.
112 this.msgQ = []; // Queue for messages that arrived out of order.
113 this.term.on("data", function (buf) {
114 ss.data.push(buf);
115 });
116 this.term.on("exit", function () {
117 ss.alive = false;
118 /* Wait for all the data to get collected */
119 setTimeout(ss.cleanup, 1000);
120 });
121 this.write = function (data, n) {
122 if (!this.alive) {
123 /* Throw some kind of exception? */
124 return;
125 }
126 if (n !== this.nrecv) {
127 console.log("Session " + this.sessid + ": Expected message " + this.nrecv + ", got " + n);
128 }
129 this.nrecv = n + 1;
130 this.term.write(data);
131 };
132 this.read = function () {
133 if (this.data.length == 0)
134 return null;
135 var pos = 0;
136 var i = 0;
137 for (i = 0; i < this.data.length; i++)
138 pos += Buffer.byteLength(this.data[i]);
139 var nbuf = new Buffer(pos);
140 var tptr;
141 pos = 0;
142 while (this.data.length > 0) {
143 tptr = new Buffer(this.data.shift());
144 tptr.copy(nbuf, pos);
145 pos += tptr.length;
146 }
147 return nbuf;
148 };
149 this.close = function () {
150 if (this.alive)
151 this.term.kill('SIGHUP');
152 };
153 this.cleanup = function () {
154 /* Call this when the child is dead. */
155 if (this.alive)
156 return;
157 /* Give the client a chance to read any leftover data. */
158 if (ss.data.length > 0)
159 setTimeout(ss.remove, 8000);
160 else
161 ss.remove();
162 };
163 this.remove = function () {
164 delete sessions[ss.sessid];
165 console.log("Session " + this.sessid + " removed.");
166 };
167 sessions[sessid] = this;
168 } 85 }
169 86
170 function randkey() { 87 function randkey() {
171 rnum = Math.floor(Math.random() * 65536 * 65536); 88 rnum = Math.floor(Math.random() * 65536 * 65536);
172 hexstr = rnum.toString(16); 89 hexstr = rnum.toString(16);
222 } catch (e) { 139 } catch (e) {
223 if (e instanceof SyntaxError) 140 if (e instanceof SyntaxError)
224 return null; 141 return null;
225 } 142 }
226 return jsonobj; 143 return jsonobj;
227 }
228
229 function login(req, res, formdata) {
230 var resheaders = {'Content-Type': 'text/plain'};
231 var sessid = randkey();
232 /* The TermSession constructor will check these thoroughly too, but
233 * you can't be too suspicious of client-supplied data. */
234 var w = 80;
235 var h = 25;
236 var t;
237 if ("w" in formdata) {
238 t = Math.floor(Number(formdata["w"]));
239 if (t > 0 && t < 256)
240 w = t;
241 }
242 if ("h" in formdata) {
243 t = Math.floor(Number(formdata["h"]));
244 if (t > 0 && t < 256)
245 h = t;
246 }
247 var nsession = new TermSession(sessid, h, w);
248 resheaders["Set-Cookie"] = "ID=" + sessid;
249 res.writeHead(200, resheaders);
250 var logindict = {"login": true, "id": sessid, "w": w, "h": h};
251 res.write(JSON.stringify(logindict));
252 res.end();
253 console.log("Started new session with key " + sessid + ", pid " + nsession.term.pid);
254 return;
255 }
256
257 function findTermSession(req) {
258 var cookies = getCookies(req);
259 if ("id" in cookies) {
260 var sessid = cookies["id"];
261 if (sessid in sessions) {
262 return sessions[sessid];
263 }
264 }
265 return null;
266 } 144 }
267 145
268 function serveStatic(req, res, fname) { 146 function serveStatic(req, res, fname) {
269 var nname = path.normalize(fname); 147 var nname = path.normalize(fname);
270 if (nname == "" || nname == "/") 148 if (nname == "" || nname == "/")
304 } 182 }
305 }); 183 });
306 return; 184 return;
307 } 185 }
308 186
309 function readFeed(res, term) {
310 res.writeHead(200, { "Content-Type": "text/plain" });
311 if (term) {
312 var answer = {};
313 var result = term.read();
314 if (result == null) {
315 answer["t"] = "n";
316 }
317 else {
318 answer["t"] = "d";
319 answer["d"] = result.toString("hex");
320 answer["n"] = term.nsend++;
321 }
322 res.write(JSON.stringify(answer));
323 res.end();
324 }
325 else {
326 sendError(res, 1);
327 }
328 }
329
330 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data" ]; 187 var errorcodes = [ "Generic Error", "Not logged in", "Invalid data" ];
331 188
332 function sendError(res, ecode) { 189 function sendError(res, ecode) {
333 res.writeHead(200, { "Content-Type": "text/plain" }); 190 res.writeHead(200, { "Content-Type": "text/plain" });
334 if (!(ecode >= 0 && ecode < errorcodes.length)) 191 if (!(ecode >= 0 && ecode < errorcodes.length))
351 req.on('data', moredata); 208 req.on('data', moredata);
352 209
353 /* This will send the response once the whole request is here. */ 210 /* This will send the response once the whole request is here. */
354 function respond() { 211 function respond() {
355 var target = url.parse(req.url).pathname; 212 var target = url.parse(req.url).pathname;
356 var cterm = findTermSession(req); 213 /* Currently only static files and WebSockets are needed. */
357 /* First figure out if the client is POSTing to a command interface. */
358 if (req.method == 'POST') { 214 if (req.method == 'POST') {
359 formdata = getFormValues(reqbody); 215 formdata = getFormValues(reqbody);
360 if (target == '/feed') { 216 res.writeHead(405, resheaders);
361 if (!cterm) { 217 res.end();
362 sendError(res, 1);
363 return;
364 }
365 if (formdata["t"] == "q") {
366 /* The client wants to quit. */
367 // FIXME need to send a message back to the client
368 cterm.close();
369 }
370 else if (formdata["t"] == "d" && typeof(formdata["d"]) == "string") {
371 /* process the keys */
372 hexstr = formdata["d"].replace(/[^0-9a-f]/gi, "");
373 if (hexstr.length % 2 != 0) {
374 sendError(res, 2);
375 return;
376 }
377 keybuf = new Buffer(hexstr, "hex");
378 cterm.write(keybuf, formdata["n"]);
379 }
380 readFeed(res, cterm);
381 }
382 else if (target == "/login") {
383 login(req, res, formdata);
384 }
385 else {
386 res.writeHead(405, resheaders);
387 res.end();
388 }
389 } 218 }
390 else if (req.method == 'GET' || req.method == 'HEAD') { 219 else if (req.method == 'GET' || req.method == 'HEAD') {
391 if (target == '/feed') { 220 serveStatic(req, res, target);
392 if (!cterm) {
393 sendError(res, 1);
394 return;
395 }
396 readFeed(res, cterm);
397 }
398 /* Default page, create a new term */
399 /* FIXME New term not created anymore, is a special case still needed? */
400 else if (target == '/') {
401 serveStatic(req, res, "/");
402 }
403 else /* Go look for it in the filesystem */
404 serveStatic(req, res, target);
405 } 221 }
406 else { /* Some other method */ 222 else { /* Some other method */
407 res.writeHead(501, resheaders); 223 res.writeHead(501, resheaders);
408 res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n"); 224 res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n");
409 res.end(); 225 res.end();
410 } 226 }
411 return; 227 return;
412 } 228 }
413 req.on('end', respond); 229 req.on('end', respond);
414
415 } 230 }
416 231
417 process.on("exit", function () { 232 process.on("exit", function () {
418 for (var sessid in sessions) { 233 for (var sessid in sessions) {
419 if (sessions[sessid].alive) 234 if (sessions[sessid].alive)