RLGWebD: excise polling.
WebSockets are supported nearly everywhere now. Listing current games and watching them are still broken.
This commit is contained in:
parent
920c6e8829
commit
c7fc6418ff
2 changed files with 36 additions and 666 deletions
364
rlgwebd.js
364
rlgwebd.js
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue