RLGWebD: excise polling.

WebSockets are supported nearly everywhere now.

Listing current games and watching them are still broken.
This commit is contained in:
John "Elwin" Edwards 2015-01-03 15:23:04 -05:00
parent 920c6e8829
commit c7fc6418ff
2 changed files with 36 additions and 666 deletions

View file

@ -63,7 +63,6 @@ var games = {
/* Global state */
var logins = {};
var sessions = {};
var clients = {};
var dglgames = {};
var allowlogin = true;
var gamemux = new events.EventEmitter();
@ -193,178 +192,6 @@ function TermSession(game, lkey, dims, handlers) {
}
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;
/* Recreate the current screen state from the session's buffer. */
this.sendQ.push({"t": "d", "n": this.nsend++,
"d": session.framebuf.toString("hex", 0, session.frameoff)});
function dataH(buf) {
var reply = {};
reply.t = "d";
reply.n = ss.nsend++;
reply.d = buf.toString("hex");
ss.sendQ.push(reply);
}
function exitH() {
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) {
if (!this.alive || typeof (n) != "number") {
return;
}
var oindex = n - this.nrecv;
if (oindex === 0) {
this.session.write(data);
this.nrecv++;
var next;
while ((next = this.recvQ.shift()) !== undefined) {
this.session.write(next);
this.nrecv++;
}
if (this.recvQ.length == 0 && this.Qtimeout) {
clearTimeout(this.Qtimeout);
this.Qtimeout = null;
}
}
else if (oindex > 0 && oindex <= 1024) {
tslog("Client %s: Stashing message %d at %d", this.id, n, oindex - 1);
this.recvQ[oindex - 1] = data;
if (!this.Qtimeout) {
var nextn = this.nrecv + this.recvQ.length + 1;
this.Qtimeout = setTimeout(this.flushQ, 30000, this, nextn);
}
}
/* Otherwise, discard it */
return;
};
this.flushQ = function (client, n) {
/* Callback for when an unreceived message times out.
* n is the first empty space that will not be given up on. */
if (!client.alive || client.nrecv >= n)
return;
client.nrecv++;
var next;
/* Clear the queue up to n */
while (client.nrecv < n) {
next = client.recvQ.shift();
if (next !== undefined)
client.session.write(next);
client.nrecv++;
}
/* Clear out anything that's ready. */
while ((next = client.recvQ.shift()) !== undefined) {
client.session.write(next);
client.nrecv++;
}
/* Now set another timeout if necessary. */
if (client.recvQ.length != 0) {
var nextn = client.nrecv + client.recvQ.length + 1;
client.Qtimeout = setTimeout(client.flushQ, 30000, client, nextn);
}
tslog("Flushing queue for player %s", player.id);
};
this.reset = function () {
/* To be called when the game is taken over. */
if (this.Qtimeout) {
clearTimeout(this.Qtimeout);
this.Qtimeout = null;
}
for (var i = 0; i < this.recvQ.length; i++) {
if (this.recvQ[i] !== undefined) {
this.session.write(this.recvQ[i]);
}
}
this.recvQ = [];
this.nrecv = 0;
this.nsend = 0;
this.sendQ = [{"t": "d", "n": this.nsend++,
"d": this.session.framebuf.toString("hex", 0, this.session.frameoff)}];
};
this.quit = function() {
if (this.alive)
this.session.close();
};
function openH(success, tag) {
if (success) {
ss.alive = true;
ss.session = sessions[tag];
ss.h = sessions[tag].h;
ss.w = sessions[tag].w;
}
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() {
ss.alive = false;
ss.sendQ.push({"t": "q"});
}
var handlers = {'open': openH, 'data': dataH, 'exit': exitH};
this.session = new TermSession(gamename, lkey, dims, handlers);
}
// Also known as WebSocketAndTermSessionClosureGlueFactory
function wsWatcher(conn, session) {
var ss = this; // is this even needed?
@ -716,103 +543,6 @@ function login(req, res, formdata) {
return;
}
function startgame(req, res, formdata) {
if (!allowlogin) {
sendError(res, 6, null);
return;
}
if (!("key" in formdata)) {
sendError(res, 2, "No key given.");
return;
}
else if (!("game" in formdata)) {
sendError(res, 2, "No game specified.");
return;
}
var lkey = String(formdata["key"]);
if (!(lkey in logins)) {
sendError(res, 1, null);
return;
}
var username = logins[lkey].name;
var gname = formdata["game"];
// If dims are not given or invalid, the constructor will handle it.
var dims = [formdata["h"], formdata["w"]];
if (!(gname in games)) {
sendError(res, 2, "No such game: " + gname);
tslog("Request for nonexistant game \"%s\"", gname);
return;
}
// A callback to pass to the game-in-progress checker.
var launch = function(err, fname) {
var nodematch = new RegExp("^" + username + ":node:");
if (fname && (fname.match(nodematch) === null)) {
/* It's being played in dgamelaunch. */
sendError(res, 4, "dgamelaunch");
tslog("%s is already playing %s", username, gname);
return;
}
// Game starting has been approved.
var respondlaunch = function(nclient, success) {
if (success) {
res.writeHead(200, {'Content-Type': 'application/json'});
var reply = {"t": "s", "id": nclient.id, "w": nclient.w, "h":
nclient.h, "p": username, "g": gname};
res.write(JSON.stringify(reply));
res.end();
}
else {
sendError(res, 5, "Failed to open TTY");
tslog("Unable to allocate TTY for %s", gname);
}
};
if (fname) {
for (var cid in clients) {
cli = clients[cid];
if ((cli instanceof Player) &&
cli.session.pname == username &&
cli.session.game.uname == gname) {
cli.reset();
respondlaunch(cli, true);
tslog("Game %d has been taken over.", cli.session.sessid);
return;
}
}
/* If there's no player, it's a WebSocket game, and shouldn't be
* seized. */
sendError(res, 4, "WebSocket");
}
else {
new Player(gname, lkey, dims, respondlaunch);
}
};
checkprogress(username, games[gname], launch, []);
}
function watch(req, res, formdata) {
if (!("g" in formdata) | !("p" in formdata)) {
sendError(res, 2, "Game or player not given");
return;
}
if (!(formdata.g in games)) {
sendError(res, 2, "No such game: " + formdata.g);
return;
}
var tag = formdata.g = "/" + formdata.p;
if (!(tag in sessions)) {
sendError(res, 7);
return;
}
var session = sessions[tag];
var watch = new Watcher(session);
var reply = {"t": "w", "w": session.w, "h": session.h,
"p": session.pname, "g": session.game.uname};
res.writeHead(200, {'Content-Type': 'application/json'});
res.write(JSON.stringify(reply));
res.end();
tslog("Game %d is being watched", tag);
}
/* Sets things up for a new user, like dgamelaunch's commands[register] */
function regsetup(username) {
function regsetup_l2(err) {
@ -876,24 +606,7 @@ function register(req, res, formdata) {
return;
}
/* Ends the game, obviously. Less obviously, stops watching the game if
* the client is a Watcher instead of a Player. */
function endgame(client, res) {
if (!client.alive) {
sendError(res, 7, null, true);
return;
}
client.quit();
// Give things some time to happen.
if (client instanceof Player)
setTimeout(readFeed, 200, client, res);
else
readFeed(client, res);
return;
}
/* Stops a running game if the request has the proper key. */
/* TODO does this still work? */
function stopgame(res, formdata) {
if (!("key" in formdata) || !(formdata["key"] in logins)) {
sendError(res, 1);
@ -940,19 +653,6 @@ function stopgame(res, formdata) {
checkprogress(pname, games[gname], checkback, []);
}
/* TODO remove polling */
function findClient(formdata, playersOnly) {
if (typeof(formdata) != "object")
return null;
if ("id" in formdata) {
var id = formdata["id"];
if (id in clients && (!playersOnly || clients[id] instanceof Player)) {
return clients[id];
}
}
return null;
}
function startProgressWatcher() {
var watchdirs = [];
for (var gname in games) {
@ -1032,22 +732,6 @@ function serveStatic(req, res, fname) {
return;
}
/* TODO remove polling */
function readFeed(client, res) {
if (!client) {
sendError(res, 7, null, true);
return;
}
var msgs = client.read();
if (!allowlogin && !msgs.length) {
sendError(res, 6, null, true);
return;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.write(JSON.stringify(msgs));
res.end();
}
/* TODO simplify by storing timestamps instead of callin stat() */
function getStatus(callback) {
var now = new Date();
@ -1272,45 +956,12 @@ function webHandler(req, res) {
var target = url.parse(req.url).pathname;
/* First figure out if the client is POSTing to a command interface. */
if (req.method == 'POST') {
if (target == '/feed') {
var client = findClient(formdata, false);
if (!client) {
sendError(res, 7, null, true);
return;
}
if (formdata.t == "q") {
/* The client wants to terminate the process. */
endgame(client, res);
return; // endgame() calls readFeed() itself.
}
else if (formdata.t == "d" && typeof(formdata.d) == "string") {
if (!(client instanceof Player)) {
sendError(res, 7, "Watching", true);
return;
}
/* process the keys */
var hexstr = formdata.d.replace(/[^0-9a-f]/gi, "");
if (hexstr.length % 2 != 0) {
sendError(res, 2, "incomplete byte", true);
return;
}
var keybuf = new Buffer(hexstr, "hex");
client.write(keybuf, formdata.n);
}
readFeed(client, res);
}
else if (target == "/login") {
if (target == "/login") {
login(req, res, formdata);
}
else if (target == "/addacct") {
register(req, res, formdata);
}
else if (target == "/play") {
startgame(req, res, formdata);
}
else if (target == "/watch") {
watch(req, res, formdata);
}
else if (target == "/quit") {
stopgame(res, formdata);
}
@ -1323,16 +974,7 @@ function webHandler(req, res) {
}
}
else if (req.method == 'GET' || req.method == 'HEAD') {
if (target == '/feed') {
if (req.method == 'HEAD') {
res.writeHead(200, {"Content-Type": "application/json"});
res.end();
}
else
sendError(res, 7, null, true);
return;
}
else if (target == '/status') {
if (target == '/status') {
statusmsg(req, res);
}
else if (target.match(/^\/uinfo\//)) {
@ -1352,7 +994,6 @@ function webHandler(req, res) {
return;
}
req.on('end', respond);
}
function wsHandler(wsRequest) {
@ -1403,6 +1044,7 @@ function wsHandler(wsRequest) {
wsRequest.reject(404, "No such resource.");
}
/* TODO use a list instead */
function pushStatus() {
getStatus(function(info) {
info["t"] = "t";