RLG-Web: make multiple watchers possible.

Split the TermSession class into the new TermSession, which handles the
PTY, and client classes, which handle HTTP sessions.  These are Player
and Watcher.  This allows multiple watchers per game, and other
improvements.
This commit is contained in:
John "Elwin" Edwards 2012-06-18 13:43:51 -07:00
parent 41b3389ec0
commit 1ffb72866a
2 changed files with 302 additions and 224 deletions

View file

@ -151,32 +151,32 @@ var msgQ = []; // Queue for out-of-order messages.
* All non-special responseTexts should be handed directly to this function. * All non-special responseTexts should be handed directly to this function.
*/ */
function processMsg(msg) { function processMsg(msg) {
var msgDict; var msgDicts;
var havedata = null; // eventual return value var havedata = null; // eventual return value
try { try {
msgDict = JSON.parse(msg); msgDicts = JSON.parse(msg);
} catch (e) { } catch (e) {
if (e instanceof SyntaxError) if (e instanceof SyntaxError)
return null; return null;
} }
if (!msgDict.t) if (msgDicts.length === 0)
return null; return false;
else if (msgDict.t == "E") { for (var j = 0; j < msgDicts.length; j++) {
if (msgDict.c == 1 || msgDict.c == 6 || msgDict.c == 7) { if (!msgDicts[j].t)
continue;
else if (msgDicts[j].t == "E") {
if (msgDicts[j].c == 1 || msgDicts[j].c == 6 || msgDicts[j].c == 7) {
gameover(); gameover();
if (msgDict.c == 1) { if (msgDicts[j].c == 1) {
logout(); logout();
} }
} }
debug(1, "Server error: " + msgDict.s); debug(1, "Server error: " + msgDicts[j].s);
}
else if (msgDict.t == "n") {
havedata = false;
} }
// A data message // A data message
else if (msgDict.t == "d"){ else if (msgDicts[j].t == "d") {
if (msgDict.n === nrecv) { if (msgDicts[j].n === nrecv) {
writeData(msgDict.d); writeData(msgDicts[j].d);
nrecv++; nrecv++;
/* Process anything in the queue that's now ready. */ /* Process anything in the queue that's now ready. */
var next; var next;
@ -185,27 +185,28 @@ function processMsg(msg) {
nrecv++; nrecv++;
} }
} }
else if (msgDict.n > nrecv) { else if (msgDicts[j].n > nrecv) {
/* The current message comes after one still missing. Queue this one /* The current message comes after one still missing. Queue this one
* for later use. */ * for later use. */
debug(1, "Got packet " + msgDict.n + ", expected " + nrecv); debug(1, "Got packet " + msgDicts[j].n + ", expected " + nrecv);
msgQ[msgDict.n - nrecv - 1] = msgDict; msgQ[msgDicts[j].n - nrecv - 1] = msgDicts[j];
} }
else { else {
/* This message's number was encountered previously. */ /* This message's number was encountered previously. */
debug(1, "Discarding packet " + msgDict.n + ", expected " + nrecv); debug(1, "Discarding packet " + msgDicts[j].n + ", expected " + nrecv);
} }
havedata = true; havedata = true;
} }
else if (msgDict.t == "T") { else if (msgDicts[j].t == "T") {
setTitle(msgDict.d); setTitle(msgDicts[j].d);
} }
else if (msgDict.t == "q") { else if (msgDicts[j].t == "q") {
gameover(); gameover();
} }
else { else {
debug(1, "Unrecognized server message " + msg); debug(1, "Unrecognized server message " + msg);
} }
}
return havedata; return havedata;
} }
@ -488,7 +489,7 @@ function startgame(game) {
if (req.readyState != 4 || req.status != 200) if (req.readyState != 4 || req.status != 200)
return; return;
var reply = JSON.parse(req.responseText); var reply = JSON.parse(req.responseText);
if (reply.t == 'l') { if (reply.t == 's') {
/* Success */ /* Success */
termemu.sessid = reply.id; termemu.sessid = reply.id;
termemu.resize(reply.h, reply.w); termemu.resize(reply.h, reply.w);

View file

@ -7,6 +7,7 @@ var net = require('net');
var url = require('url'); var url = require('url');
var path = require('path'); var path = require('path');
var fs = require('fs'); var fs = require('fs');
var events = require('events');
var child_process = require('child_process'); var child_process = require('child_process');
var daemon = require(path.join(localModules, "daemon")); var daemon = require(path.join(localModules, "daemon"));
@ -52,39 +53,46 @@ var games = {
/* Global state */ /* Global state */
var logins = {}; var logins = {};
var sessions = {}; var sessions = {};
var clients = {};
var allowlogin = true; var allowlogin = true;
var nextsession = 0;
/* Constructor for TermSessions. Note that it opens the terminal and /* Constructor. A TermSession handles a pty and the game running on it.
* adds itself to the sessions dict. It currently assumes the user has * game: (String) Name of the game to launch.
* been authenticated. * lkey: (String, key) The user's id, a key into logins.
* dims: (Array [Number, Number]) Height and width of the pty.
* handlers: (Object) Key-value pairs, event names and functions to
* install to handle them.
* Events:
* "open": Emitted on startup. Parameters: success (Boolean)
* "data": Data generated by child. Parameters: buf (Buffer)
* "exit": Child terminated. Parameters: exitcode, signal
*/ */
/* TODO take a callback, or emit success/err events. */ function TermSession(game, lkey, dims, handlers) {
function TermSession(game, user, dims, lkey) { var ss = this;
/* First make sure starting the game will work. */ /* Subclass EventEmitter to do the hard work. */
events.EventEmitter.call(this);
for (var evname in handlers)
this.on(evname, handlers[evname]);
/* Don't launch anything that's not a real game. */
if (game in games) { if (game in games) {
this.game = games[game]; this.game = games[game];
} }
else { else {
// TODO: throw an exception instead this.emit('open', false);
return null; return;
} }
this.player = String(user); if (lkey in logins) {
this.key = lkey; this.key = lkey;
/* This order seems to best avoid race conditions... */ this.pname = logins[lkey].name;
this.alive = false; }
// A kludge until TermSession is rewritten to handle real watching. else {
this.sendq = false; this.emit('open', false);
this.sessid = randkey(2); return;
while (this.sessid in sessions) {
this.sessid = randkey(2);
} }
/* Grab a spot in the sessions table. */ /* Grab a spot in the sessions table. */
this.sessid = nextsession++;
sessions[this.sessid] = this; sessions[this.sessid] = this;
/* State for messaging. */
this.nsend = 0;
this.nrecv = 0;
this.msgQ = []
this.Qtimeout = null;
/* Set up the sizes. */ /* Set up the sizes. */
this.w = Math.floor(Number(dims[1])); this.w = Math.floor(Number(dims[1]));
if (!(this.w > 0 && this.w < 256)) if (!(this.w > 0 && this.w < 256))
@ -98,22 +106,21 @@ function TermSession(game, user, dims, lkey) {
childenv[key] = process.env[key]; childenv[key] = process.env[key];
} }
childenv["PTYHELPER"] = String(this.h) + "x" + String(this.w); childenv["PTYHELPER"] = String(this.h) + "x" + String(this.w);
/* TODO handle tty-opening errors */ args = [this.game.path, "-n", this.pname];
/* TODO make argument-finding into a method */
args = [this.game.path, "-n", user.toString()];
this.child = child_process.spawn("/bin/ptyhelper", args, {"env": childenv}); this.child = child_process.spawn("/bin/ptyhelper", args, {"env": childenv});
var ss = this; this.emit('open', true);
this.alive = true;
this.data = [];
/* Set up the lockfile and ttyrec */ /* Set up the lockfile and ttyrec */
var ts = timestamp(); var ts = timestamp();
var progressdir = "/dgldir/inprogress-" + this.game.uname; var progressdir = "/dgldir/inprogress-" + this.game.uname;
this.lock = path.join(progressdir, this.player + ":node:" + ts + ".ttyrec"); this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec");
var lmsg = this.child.pid.toString() + '\n' + this.w + '\n' + this.h + '\n'; var lmsg = this.child.pid.toString() + '\n' + this.w + '\n' + this.h + '\n';
fs.writeFile(this.lock, lmsg, "utf8"); fs.writeFile(this.lock, lmsg, "utf8");
var ttyrec = path.join("/dgldir/ttyrec", this.player, this.game.uname, var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.game.uname,
ts + ".ttyrec"); ts + ".ttyrec");
this.record = fs.createWriteStream(ttyrec, { mode: 0664 }); this.record = fs.createWriteStream(ttyrec, { mode: 0664 });
logins[lkey].sessions.push(this.sessid);
tslog("%s playing %s (index %d, pid %d)", this.pname, this.game.uname,
this.sessid, this.child.pid);
/* END setup */ /* END setup */
function ttyrec_chunk(buf) { function ttyrec_chunk(buf) {
var ts = new Date(); var ts = new Date();
@ -123,115 +130,180 @@ function TermSession(game, user, dims, lkey) {
chunk.writeUInt32LE(1000 * (ts.getTime() % 1000), 4); chunk.writeUInt32LE(1000 * (ts.getTime() % 1000), 4);
chunk.writeUInt32LE(buf.length, 8); chunk.writeUInt32LE(buf.length, 8);
buf.copy(chunk, 12); buf.copy(chunk, 12);
ss.data.push(chunk);
ss.record.write(chunk); ss.record.write(chunk);
ss.emit('data', buf);
} }
this.child.stdout.on("data", ttyrec_chunk); this.child.stdout.on("data", ttyrec_chunk);
this.child.stderr.on("data", ttyrec_chunk); this.child.stderr.on("data", ttyrec_chunk);
this.write = function(data) {
this.child.stdin.write(data);
};
this.child.on("exit", function (code, signal) { this.child.on("exit", function (code, signal) {
ss.exitcode = (code != null ? code : 255);
ss.alive = false;
fs.unlink(ss.lock); fs.unlink(ss.lock);
/* Wait for all the data to get collected */ ss.record.end();
setTimeout(ss.cleanup, 1000); ss.emit('exit', code, signal);
var id = ss.sessid;
delete sessions[id];
tslog("Session %s ended.", id);
}); });
this.close = function () {
this.child.kill('SIGHUP');
};
}
TermSession.prototype = new events.EventEmitter();
function Watcher(session) {
var ss = this; // that
this.session = session;
this.alive = true;
/* State for messaging. */
this.nsend = 0;
this.sendQ = [];
/* Get a place in the table. */
this.id = randkey(2);
while (this.id in clients) {
this.id = randkey(2);
}
clients[this.id] = this;
function dataH(buf) {
var reply = {};
reply.t = "d";
reply.n = ss.nsend++;
reply.d = buf.toString("hex");
ss.sendQ.push(reply);
}
function exitH(code, signal) {
ss.alive = false;
ss.sendQ.push({"t": "q"});
}
session.on('data', dataH);
session.on('exit', exitH);
this.read = function() {
/* Returns an array of all outstanding messages, empty if none. */
var temp = this.sendQ;
this.sendQ = [];
/* Clean up if finished. */
if (!this.alive) {
delete clients[this.id];
}
return temp;
};
this.quit = function() {
this.session.removeListener('data', dataH);
this.session.removeListener('exit', exitH);
delete clients[this.id];
};
}
function Player(gamename, lkey, dims, callback) {
var ss = this;
this.alive = false;
/* State for messaging. */
this.nsend = 0;
this.nrecv = 0;
this.sendQ = [];
this.recvQ = []
this.Qtimeout = null;
/* Get a place in the table. */
this.id = randkey(2);
while (this.id in clients) {
this.id = randkey(2);
}
clients[this.id] = this;
this.read = function() {
var temp = this.sendQ;
this.sendQ = [];
/* Clean up if finished. */
if (!this.alive) {
clearTimeout(this.Qtimeout);
delete clients[this.id];
}
return temp;
};
this.write = function (data, n) { this.write = function (data, n) {
if (!this.alive || typeof (n) != "number") { if (!this.alive || typeof (n) != "number") {
return; return;
} }
//console.log("Got message " + n);
var oindex = n - this.nrecv; var oindex = n - this.nrecv;
if (oindex === 0) { if (oindex === 0) {
//console.log("Writing message " + n); this.session.write(data);
this.child.stdin.write(data);
this.nrecv++; this.nrecv++;
var next; var next;
while ((next = this.msgQ.shift()) !== undefined) { while ((next = this.recvQ.shift()) !== undefined) {
//console.log("Writing message " + this.nrecv); this.session.write(next);
this.child.stdin.write(next);
this.nrecv++; this.nrecv++;
} }
if (this.msgQ.length == 0 && this.Qtimeout) { if (this.recvQ.length == 0 && this.Qtimeout) {
clearTimeout(this.Qtimeout); clearTimeout(this.Qtimeout);
this.Qtimeout = null; this.Qtimeout = null;
} }
} }
else if (oindex > 0 && oindex <= 1024) { else if (oindex > 0 && oindex <= 1024) {
tslog("Stashing message %d at %d", n, oindex - 1); tslog("Client %s: Stashing message %d at %d", this.id, n, oindex - 1);
this.msgQ[oindex - 1] = data; this.recvQ[oindex - 1] = data;
if (!this.Qtimeout) { if (!this.Qtimeout) {
var nextn = this.nrecv + this.msgQ.length + 1; var nextn = this.nrecv + this.recvQ.length + 1;
this.Qtimeout = setTimeout(this.flushQ, 30000, this, nextn); this.Qtimeout = setTimeout(this.flushQ, 30000, this, nextn);
} }
} }
/* Otherwise, discard it */ /* Otherwise, discard it */
return; return;
}; };
this.flushQ = function (session, n) { this.flushQ = function (client, n) {
/* Callback for when an unreceived message times out. /* Callback for when an unreceived message times out.
* n is the first empty space that will not be given up on. */ * n is the first empty space that will not be given up on. */
if (!session.alive) if (!client.alive || client.nrecv >= n)
return; return;
session.nrecv++; client.nrecv++;
var next; var next;
/* Clear the queue up to n */ /* Clear the queue up to n */
while (session.nrecv < n) { while (client.nrecv < n) {
next = session.msgQ.shift(); next = client.recvQ.shift();
if (next !== undefined) if (next !== undefined)
session.child.stdin.write(next); client.session.write(next);
session.nrecv++; client.nrecv++;
} }
/* Clear out anything that's ready. */ /* Clear out anything that's ready. */
while ((next = session.msgQ.shift()) !== undefined) { while ((next = client.recvQ.shift()) !== undefined) {
session.child.stdin.write(next); client.session.write(next);
session.nrecv++; client.nrecv++;
} }
/* Now set another timeout if necessary. */ /* Now set another timeout if necessary. */
if (session.msgQ.length != 0) { if (client.recvQ.length != 0) {
var nextn = session.nrecv + session.msgQ.length + 1; var nextn = client.nrecv + client.recvQ.length + 1;
session.Qtimeout = setTimeout(session.flushQ, 30000, session, nextn); client.Qtimeout = setTimeout(client.flushQ, 30000, client, nextn);
} }
tslog("Flushing queue for session %s", session.sessid); tslog("Flushing queue for player %s", player.id);
}; };
this.read = function () { this.quit = function() {
if (this.data.length == 0)
return null;
var pos = 0;
var i = 0;
for (i = 0; i < this.data.length; i++)
pos += this.data[i].length - 12;
var nbuf = new Buffer(pos);
var tptr;
pos = 0;
while (this.data.length > 0) {
tptr = this.data.shift();
tptr.copy(nbuf, pos, 12);
pos += tptr.length - 12;
}
return nbuf;
};
this.close = function () {
if (this.alive) if (this.alive)
this.child.kill('SIGHUP'); this.session.close();
};
this.cleanup = function () {
/* Call this when the child is dead. */
if (ss.alive)
return;
ss.record.end();
/* Give the client a chance to read any leftover data. */
if (ss.data.length > 0 || !ss.sendq)
setTimeout(ss.remove, 8000);
else
ss.remove();
};
this.remove = function () {
var id = ss.sessid;
delete sessions[id];
tslog("Session %s removed.", id);
}; };
function openH(success) {
if (success) {
ss.alive = true;
}
callback(ss, success);
}
function dataH(chunk) {
var reply = {};
reply.t = "d";
reply.n = ss.nsend++;
reply.d = chunk.toString("hex");
ss.sendQ.push(reply);
}
function exitH(code, signal) {
ss.alive = false;
ss.sendQ.push({"t": "q"});
}
var handlers = {'open': openH, 'data': dataH, 'exit': exitH};
this.session = new TermSession(gamename, lkey, dims, handlers);
} }
/* Some functions which check whether a player is currently playing or
* has a saved game. Maybe someday they will provide information on
* the game. */
function checkprogress(user, game, callback, args) { function checkprogress(user, game, callback, args) {
var progressdir = "/dgldir/inprogress-" + game.uname; var progressdir = "/dgldir/inprogress-" + game.uname;
fs.readdir(progressdir, function(err, files) { fs.readdir(progressdir, function(err, files) {
@ -359,11 +431,9 @@ function getMsg(posttext) {
function reaper() { function reaper() {
var now = new Date(); var now = new Date();
function reapcheck(session) { function reapcheck(session) {
if (!session.alive)
return;
fs.fstat(session.record.fd, function (err, stats) { fs.fstat(session.record.fd, function (err, stats) {
if (!err && now - stats.mtime > playtimeout) { if (!err && now - stats.mtime > playtimeout) {
tslog("Reaping %s", session.sessid); tslog("Reaping session %s", session.sessid);
/* Dissociate it with its login name. */ /* Dissociate it with its login name. */
var sn = logins[session.key].sessions.indexOf(session.sessid); var sn = logins[session.key].sessions.indexOf(session.sessid);
if (sn >= 0) { if (sn >= 0) {
@ -403,7 +473,7 @@ function reaper() {
if (expired.length > 0) { if (expired.length > 0) {
logins[lkey].ts = new Date(now); logins[lkey].ts = new Date(now);
for (var j = 0; j < expired.length; j++) { for (var j = 0; j < expired.length; j++) {
targarray.splice(targarray.indexOf(expired[j], 1)); targarray.splice(targarray.indexOf(expired[j]), 1);
} }
} }
} }
@ -412,15 +482,15 @@ function reaper() {
function login(req, res, formdata) { function login(req, res, formdata) {
if (!allowlogin) { if (!allowlogin) {
sendError(res, 6, null); sendError(res, 6, null, false);
return; return;
} }
if (!("name" in formdata)) { if (!("name" in formdata)) {
sendError(res, 2, "Username not given."); sendError(res, 2, "Username not given.", false);
return; return;
} }
else if (!("pw" in formdata)) { else if (!("pw" in formdata)) {
sendError(res, 2, "Password not given."); sendError(res, 2, "Password not given.", false);
return; return;
} }
var username = String(formdata["name"]); var username = String(formdata["name"]);
@ -478,6 +548,7 @@ function startgame(req, res, formdata) {
} }
var username = logins[lkey].name; var username = logins[lkey].name;
var gname = formdata["game"]; var gname = formdata["game"];
// If dims are not given or invalid, the constructor will handle it.
var dims = [formdata["h"], formdata["w"]]; var dims = [formdata["h"], formdata["w"]];
if (!(gname in games)) { if (!(gname in games)) {
sendError(res, 2, "No such game: " + gname); sendError(res, 2, "No such game: " + gname);
@ -492,25 +563,43 @@ function startgame(req, res, formdata) {
return; return;
} }
// Game starting has been approved. // Game starting has been approved.
var nsession = new TermSession(gname, username, dims, lkey); var respondlaunch = function(nclient, success) {
if (nsession) { if (success) {
res.writeHead(200, {'Content-Type': 'application/json'}); res.writeHead(200, {'Content-Type': 'application/json'});
var reply = {"t": "l", "id": nsession.sessid, "w": nsession.w, "h": var reply = {"t": "s", "id": nclient.id, "w": nclient.w, "h":
nsession.h}; nclient.h};
res.write(JSON.stringify(reply)); res.write(JSON.stringify(reply));
res.end(); res.end();
tslog("%s playing %s (key %s, pid %d)", username, gname,
nsession.sessid, nsession.child.pid);
logins[lkey].sessions.push(nsession.sessid);
} }
else { else {
sendError(res, 5, "Failed to open TTY"); sendError(res, 5, "Failed to open TTY");
tslog("Unable to allocate TTY for %s", gname); tslog("Unable to allocate TTY for %s", gname);
} }
} };
new Player(gname, lkey, dims, respondlaunch);
};
checkprogress(username, games[gname], launch, []); checkprogress(username, games[gname], launch, []);
} }
function watch(req, res, formdata) {
if (!("n" in formdata)) {
sendError(res, 2, "Game number not given");
return;
}
var gamenumber = Number(formdata["n"]);
if (!(gamenumber in sessions)) {
sendError(res, 7);
return;
}
var session = sessions[gamenumber];
var watch = new Watcher(session);
var reply = {"t": "w", "id": watch.id, "w": session.w, "h": session.h};
res.writeHead(200, {'Content-Type': 'application/json'});
res.write(JSON.stringify(reply));
res.end();
tslog("Game %d is being watched (key %s)", gamenumber, watch.id);
}
/* Sets things up for a new user, like dgamelaunch's commands[register] */ /* Sets things up for a new user, like dgamelaunch's commands[register] */
function regsetup(username) { function regsetup(username) {
function regsetup_l2(err) { function regsetup_l2(err) {
@ -574,27 +663,27 @@ function register(req, res, formdata) {
return; return;
} }
function endgame(term, res) { /* Ends the game, obviously. Less obviously, stops watching the game if
if (!term.alive) { * the client is a Watcher instead of a Player. */
sendError(res, 7, null); function endgame(client, res) {
if (!client.alive) {
sendError(res, 7, null, true);
return; return;
} }
term.close(); client.quit();
var resheaders = {'Content-Type': 'application/json'}; // Give things some time to happen.
res.writeHead(200, resheaders); if (client instanceof Player)
res.write(JSON.stringify({"t": "q"})); setTimeout(readFeed, 200, client, res);
res.end();
term.sendq = true;
return; return;
} }
function findTermSession(formdata) { function findClient(formdata, playersOnly) {
if (typeof(formdata) != "object") if (typeof(formdata) != "object")
return null; return null;
if ("id" in formdata) { if ("id" in formdata) {
var sessid = formdata["id"]; var id = formdata["id"];
if (sessid in sessions) { if (id in clients && (!playersOnly || clients[id] instanceof Player)) {
return sessions[sessid]; return clients[id];
} }
} }
return null; return null;
@ -648,48 +737,30 @@ function serveStatic(req, res, fname) {
return; return;
} }
function readFeed(res, term) { function readFeed(client, res) {
if (term) { if (!client) {
var reply = {}; sendError(res, 7, null, true);
var result = term.read();
if (result == null) {
if (term.alive)
reply.t = "n";
else {
if (allowlogin) {
reply.t = "q";
term.sendq = true;
}
else {
sendError(res, 6, null);
return; return;
} }
} var msgs = client.read();
} if (!allowlogin && !msgs.length) {
else { sendError(res, 6, null, true);
reply.t = "d"; return;
reply.n = term.nsend++;
reply.d = result.toString("hex");
} }
res.writeHead(200, { "Content-Type": "application/json" }); res.writeHead(200, { "Content-Type": "application/json" });
res.write(JSON.stringify(reply)); res.write(JSON.stringify(msgs));
res.end(); res.end();
} }
else {
sendError(res, 7, null);
}
}
function statusmsg(req, res) { function statusmsg(req, res) {
var reply = {"s": allowlogin, "g": []}; var reply = {"s": allowlogin, "g": []};
for (var sessid in sessions) { for (var sessid in sessions) {
if (sessions[sessid].alive) {
var gamedesc = {}; var gamedesc = {};
gamedesc["p"] = sessions[sessid].player; gamedesc["n"] = sessid;
gamedesc["p"] = sessions[sessid].pname;
gamedesc["g"] = sessions[sessid].game.name; gamedesc["g"] = sessions[sessid].game.name;
reply["g"].push(gamedesc); reply["g"].push(gamedesc);
} }
}
res.writeHead(200, { "Content-Type": "application/json" }); res.writeHead(200, { "Content-Type": "application/json" });
if (req.method != 'HEAD') if (req.method != 'HEAD')
res.write(JSON.stringify(reply)); res.write(JSON.stringify(reply));
@ -723,7 +794,7 @@ var errorcodes = [ "Generic Error", "Not logged in", "Invalid data",
"Login failed", "Already playing", "Game launch failed", "Login failed", "Already playing", "Game launch failed",
"Server shutting down", "Game not in progress" ]; "Server shutting down", "Game not in progress" ];
function sendError(res, ecode, msg) { function sendError(res, ecode, msg, box) {
res.writeHead(200, { "Content-Type": "application/json" }); res.writeHead(200, { "Content-Type": "application/json" });
var edict = {"t": "E"}; var edict = {"t": "E"};
if (!(ecode < errorcodes.length && ecode > 0)) if (!(ecode < errorcodes.length && ecode > 0))
@ -732,10 +803,14 @@ function sendError(res, ecode, msg) {
edict["s"] = errorcodes[ecode]; edict["s"] = errorcodes[ecode];
if (msg) if (msg)
edict["s"] += ": " + msg; edict["s"] += ": " + msg;
if (box)
res.write(JSON.stringify([edict]));
else
res.write(JSON.stringify(edict)); res.write(JSON.stringify(edict));
res.end(); res.end();
} }
// TODO new-objects done to here
function webHandler(req, res) { function webHandler(req, res) {
/* default headers for the response */ /* default headers for the response */
var resheaders = {'Content-Type': 'text/html'}; var resheaders = {'Content-Type': 'text/html'};
@ -753,30 +828,33 @@ function webHandler(req, res) {
function respond() { function respond() {
formdata = getMsg(reqbody); formdata = getMsg(reqbody);
var target = url.parse(req.url).pathname; var target = url.parse(req.url).pathname;
var cterm = findTermSession(formdata);
/* First figure out if the client is POSTing to a command interface. */ /* First figure out if the client is POSTing to a command interface. */
if (req.method == 'POST') { if (req.method == 'POST') {
if (target == '/feed') { if (target == '/feed') {
if (!cterm) { var client = findClient(formdata, false);
sendError(res, 7, null); if (!client) {
sendError(res, 7, null, true);
return; return;
} }
if (formdata.t == "q") { if (formdata.t == "q") {
/* The client wants to terminate the process. */ /* The client wants to terminate the process. */
endgame(cterm, res); endgame(client, res);
} }
else if (formdata.t == "d" && typeof(formdata.d) == "string") { else if (formdata.t == "d" && typeof(formdata.d) == "string") {
if (!(client instanceof Player)) {
sendError(res, 7, "Watching", true);
return;
}
/* process the keys */ /* process the keys */
hexstr = formdata.d.replace(/[^0-9a-f]/gi, ""); hexstr = formdata.d.replace(/[^0-9a-f]/gi, "");
if (hexstr.length % 2 != 0) { if (hexstr.length % 2 != 0) {
sendError(res, 2, "incomplete byte"); sendError(res, 2, "incomplete byte", true);
return; return;
} }
keybuf = new Buffer(hexstr, "hex"); keybuf = new Buffer(hexstr, "hex");
/* TODO OoO correction */ client.write(keybuf, formdata.n);
cterm.write(keybuf, formdata.n);
} }
readFeed(res, cterm); readFeed(client, res);
} }
else if (target == "/login") { else if (target == "/login") {
login(req, res, formdata); login(req, res, formdata);
@ -787,6 +865,9 @@ function webHandler(req, res) {
else if (target == "/play") { else if (target == "/play") {
startgame(req, res, formdata); startgame(req, res, formdata);
} }
else if (target == "/watch") {
watch(req, res, formdata);
}
else { else {
res.writeHead(405, resheaders); res.writeHead(405, resheaders);
res.end(); res.end();
@ -797,14 +878,11 @@ function webHandler(req, res) {
if (req.method == 'HEAD') { if (req.method == 'HEAD') {
res.writeHead(200, {"Content-Type": "application/json"}); res.writeHead(200, {"Content-Type": "application/json"});
res.end(); res.end();
return;
} }
if (!cterm) { else
sendError(res, 7, null); sendError(res, 7, null, true);
return; return;
} }
readFeed(res, cterm);
}
else if (target == '/status') { else if (target == '/status') {
statusmsg(req, res); statusmsg(req, res);
} }
@ -841,13 +919,12 @@ function conHandler(chunk) {
for (var sessid in sessions) { for (var sessid in sessions) {
sessions[sessid].close(); sessions[sessid].close();
} }
setTimeout(shutdown, 10000); setTimeout(shutdown, 2000);
} }
} }
process.on("exit", function () { process.on("exit", function () {
for (var sessid in sessions) { for (var sessid in sessions) {
if (sessions[sessid].alive)
sessions[sessid].child.kill('SIGHUP'); sessions[sessid].child.kill('SIGHUP');
} }
tslog("Quitting..."); tslog("Quitting...");