comparison rlgwebd.js @ 158:9961a538c00e

rlgwebd.js: get rid of numerical game identifiers. Games will be indentified by gamename/username pairs. This will allow better interoperability with dgamelaunch. Polling clients are no longer supported; the code remnants need to be removed. The reaper() function will likely crash. Unexpectedly, the WebSocket client still works well enough to play. Watching and listing current games are probably broken.
author John "Elwin" Edwards
date Thu, 01 Jan 2015 15:56:22 -0500
parents e7f809f06c5c
children a613380ffdc2
comparison
equal deleted inserted replaced
157:e7f809f06c5c 158:9961a538c00e
64 var logins = {}; 64 var logins = {};
65 var sessions = {}; 65 var sessions = {};
66 var clients = {}; 66 var clients = {};
67 var dglgames = {}; 67 var dglgames = {};
68 var allowlogin = true; 68 var allowlogin = true;
69 var nextsession = 0;
70 var gamemux = new events.EventEmitter(); 69 var gamemux = new events.EventEmitter();
71 70
72 /* Constructor. A TermSession handles a pty and the game running on it. 71 /* Constructor. A TermSession handles a pty and the game running on it.
73 * game: (String) Name of the game to launch. 72 * game: (String) Name of the game to launch.
74 * lkey: (String, key) The user's id, a key into logins. 73 * lkey: (String, key) The user's id, a key into logins.
101 else { 100 else {
102 this.emit('open', false); 101 this.emit('open', false);
103 return; 102 return;
104 } 103 }
105 /* Grab a spot in the sessions table. */ 104 /* Grab a spot in the sessions table. */
106 this.sessid = nextsession++; 105 sessions[this.game.uname + "/" + this.pname] = this;
107 sessions[this.sessid] = this;
108 /* Set up the sizes. */ 106 /* Set up the sizes. */
109 this.w = Math.floor(Number(dims[1])); 107 this.w = Math.floor(Number(dims[1]));
110 if (!(this.w > 0 && this.w < 256)) 108 if (!(this.w > 0 && this.w < 256))
111 this.w = 80; 109 this.w = 80;
112 this.h = Math.floor(Number(dims[0])); 110 this.h = Math.floor(Number(dims[0]));
119 } 117 }
120 var args = ["-n", this.pname]; 118 var args = ["-n", this.pname];
121 var spawnopts = {"env": childenv, "cwd": "/", "rows": this.h, "cols": this.w, 119 var spawnopts = {"env": childenv, "cwd": "/", "rows": this.h, "cols": this.w,
122 "name": "xterm-256color"}; 120 "name": "xterm-256color"};
123 this.term = pty.spawn(this.game.path, args, spawnopts); 121 this.term = pty.spawn(this.game.path, args, spawnopts);
124 tslog("%s playing %s (index %d, pid %d)", this.pname, this.game.uname, 122 tslog("%s playing %s (pid %d)", this.pname, this.game.uname, this.term.pid);
125 this.sessid, this.term.pid); 123 this.emit('open', true, this.game.uname, this.pname);
126 this.emit('open', true, this.sessid); 124 gamemux.emit('begin', this.game.uname, this.pname);
127 gamemux.emit('begin', this.sessid, this.pname, this.game.uname);
128 /* Set up the lockfile and ttyrec */ 125 /* Set up the lockfile and ttyrec */
129 var ts = timestamp(); 126 var ts = timestamp();
130 var progressdir = path.join("/dgldir/inprogress", this.game.uname); 127 var progressdir = path.join("/dgldir/inprogress", this.game.uname);
131 this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec"); 128 this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec");
132 var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n'; 129 var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n';
136 this.record = fs.createWriteStream(ttyrec, { mode: 0664 }); 133 this.record = fs.createWriteStream(ttyrec, { mode: 0664 });
137 /* Holds the output since the last screen clear, so watchers can begin 134 /* Holds the output since the last screen clear, so watchers can begin
138 * with a complete screen. */ 135 * with a complete screen. */
139 this.framebuf = new Buffer(1024); 136 this.framebuf = new Buffer(1024);
140 this.frameoff = 0; 137 this.frameoff = 0;
141 logins[lkey].sessions.push(this.sessid); 138 logins[lkey].sessions.push(this.game.uname + "/" + this.pname);
142 /* END setup */ 139 /* END setup */
143 function ttyrec_chunk(datastr) { 140 function ttyrec_chunk(datastr) {
144 var ts = new Date(); 141 var ts = new Date();
145 var buf = new Buffer(datastr); 142 var buf = new Buffer(datastr);
146 var chunk = new Buffer(buf.length + 12); 143 var chunk = new Buffer(buf.length + 12);
164 while (this.framebuf.length < chunk.length + this.frameoff) { 161 while (this.framebuf.length < chunk.length + this.frameoff) {
165 var nbuf = new Buffer(this.framebuf.length * 2); 162 var nbuf = new Buffer(this.framebuf.length * 2);
166 this.framebuf.copy(nbuf, 0, 0, this.frameoff); 163 this.framebuf.copy(nbuf, 0, 0, this.frameoff);
167 this.framebuf = nbuf; 164 this.framebuf = nbuf;
168 if (this.framebuf.length > 65536) { 165 if (this.framebuf.length > 65536) {
169 tslog("Warning: Game %d frame buffer at %d bytes", this.sessid, 166 tslog("Warning: Game %d frame buffer at %d bytes", this.tag(),
170 this.framebuf.length); 167 this.framebuf.length);
171 } 168 }
172 } 169 }
173 chunk.copy(this.framebuf, this.frameoff); 170 chunk.copy(this.framebuf, this.frameoff);
174 this.frameoff += chunk.length; 171 this.frameoff += chunk.length;
175 }; 172 };
176 this.write = function(data) { 173 this.write = function(data) {
177 this.term.write(data); 174 this.term.write(data);
178 }; 175 };
176 this.tag = function() {
177 return this.game.uname + "/" + this.pname;
178 };
179 // Teardown. 179 // Teardown.
180 this.term.on("exit", function () { 180 this.term.on("exit", function () {
181 var id = ss.sessid; 181 var tag = ss.tag();
182 fs.unlink(ss.lock); 182 fs.unlink(ss.lock);
183 ss.record.end(); 183 ss.record.end();
184 ss.emit('exit'); 184 ss.emit('exit');
185 gamemux.emit('end', id, ss.pname, ss.game.uname); 185 gamemux.emit('end', ss.game.uname, ss.pname);
186 delete sessions[id]; 186 delete sessions[tag];
187 tslog("Game %s ended.", id); 187 tslog("Game %s ended.", tag);
188 }); 188 });
189 this.close = function () { 189 this.close = function () {
190 if (this.sessid in sessions) 190 if (this.tag() in sessions)
191 this.term.kill('SIGHUP'); 191 this.term.kill('SIGHUP');
192 }; 192 };
193 } 193 }
194 TermSession.prototype = new events.EventEmitter(); 194 TermSession.prototype = new events.EventEmitter();
195 195
339 }; 339 };
340 this.quit = function() { 340 this.quit = function() {
341 if (this.alive) 341 if (this.alive)
342 this.session.close(); 342 this.session.close();
343 }; 343 };
344 function openH(success, id) { 344 function openH(success, tag) {
345 if (success) { 345 if (success) {
346 ss.alive = true; 346 ss.alive = true;
347 ss.session = sessions[id]; 347 ss.session = sessions[tag];
348 ss.h = sessions[id].h; 348 ss.h = sessions[tag].h;
349 ss.w = sessions[id].w; 349 ss.w = sessions[tag].w;
350 } 350 }
351 callback(ss, success); 351 callback(ss, success);
352 } 352 }
353 function dataH(chunk) { 353 function dataH(chunk) {
354 var reply = {}; 354 var reply = {};
378 session.on('data', dataH); 378 session.on('data', dataH);
379 session.on('exit', exitH); 379 session.on('exit', exitH);
380 conn.on('close', function(code, desc) { 380 conn.on('close', function(code, desc) {
381 session.removeListener('data', dataH); 381 session.removeListener('data', dataH);
382 session.removeListener('exit', exitH); 382 session.removeListener('exit', exitH);
383 if (session.sessid in sessions) 383 if (session.tag() in sessions)
384 tslog("A WebSocket watcher has left game %d", session.sessid); 384 tslog("A WebSocket watcher has left game %d", session.tag());
385 }); 385 });
386 conn.sendUTF(JSON.stringify({ 386 conn.sendUTF(JSON.stringify({
387 "t": "w", "w": session.w, "h": session.h, 387 "t": "w", "w": session.w, "h": session.h,
388 "p": session.pname, "g": session.game.uname 388 "p": session.pname, "g": session.game.uname
389 })); 389 }));
411 } 411 }
412 function closeH() { 412 function closeH() {
413 session.close(); 413 session.close();
414 } 414 }
415 /* These listen on the TermSession. */ 415 /* These listen on the TermSession. */
416 function openH(success, id) { 416 function openH(success, gname, pname) {
417 if (success) { 417 if (success) {
418 var reply = {"t": "s", "id": id, "w": sessions[id].w, "h": 418 var tag = gname + "/" + pname;
419 sessions[id].h, "p": sessions[id].pname, "g": game}; 419 var reply = {"t": "s", "tag": tag, "w": sessions[tag].w, "h":
420 sessions[tag].h, "p": pname, "g": gname};
420 conn = wsReq.accept(null, wsReq.origin); 421 conn = wsReq.accept(null, wsReq.origin);
421 conn.sendUTF(JSON.stringify(reply)); 422 conn.sendUTF(JSON.stringify(reply));
422 conn.on('message', messageH); 423 conn.on('message', messageH);
423 conn.on('close', closeH); 424 conn.on('close', closeH);
424 } 425 }
627 if (msgObj.type != "utf8") 628 if (msgObj.type != "utf8")
628 return {}; 629 return {};
629 return getMsg(msgObj.utf8Data); 630 return getMsg(msgObj.utf8Data);
630 } 631 }
631 632
633 /* FIXME sessid removal */
632 function reaper() { 634 function reaper() {
635 return; // TODO figure out if this function is useful
633 var now = new Date(); 636 var now = new Date();
634 function reapcheck(session) { 637 function reapcheck(session) {
635 fs.fstat(session.record.fd, function (err, stats) { 638 fs.fstat(session.record.fd, function (err, stats) {
636 if (!err && now - stats.mtime > playtimeout) { 639 if (!err && now - stats.mtime > playtimeout) {
637 tslog("Reaping session %s", session.sessid); 640 tslog("Reaping session %s", session.sessid);
785 }; 788 };
786 checkprogress(username, games[gname], launch, []); 789 checkprogress(username, games[gname], launch, []);
787 } 790 }
788 791
789 function watch(req, res, formdata) { 792 function watch(req, res, formdata) {
790 if (!("n" in formdata)) { 793 if (!("g" in formdata) | !("p" in formdata)) {
791 sendError(res, 2, "Game number not given"); 794 sendError(res, 2, "Game or player not given");
792 return; 795 return;
793 } 796 }
794 var gamenumber = Number(formdata["n"]); 797 if (!(formdata.g in games)) {
795 if (!(gamenumber in sessions)) { 798 sendError(res, 2, "No such game: " + formdata.g);
799 return;
800 }
801 var tag = formdata.g = "/" + formdata.p;
802 if (!(tag in sessions)) {
796 sendError(res, 7); 803 sendError(res, 7);
797 return; 804 return;
798 } 805 }
799 var session = sessions[gamenumber]; 806 var session = sessions[tag];
800 var watch = new Watcher(session); 807 var watch = new Watcher(session);
801 var reply = {"t": "w", "id": watch.id, "w": session.w, "h": session.h, 808 var reply = {"t": "w", "w": session.w, "h": session.h,
802 "p": session.pname, "g": session.game.uname}; 809 "p": session.pname, "g": session.game.uname};
803 res.writeHead(200, {'Content-Type': 'application/json'}); 810 res.writeHead(200, {'Content-Type': 'application/json'});
804 res.write(JSON.stringify(reply)); 811 res.write(JSON.stringify(reply));
805 res.end(); 812 res.end();
806 tslog("Game %d is being watched (key %s)", gamenumber, watch.id); 813 tslog("Game %d is being watched", tag);
807 } 814 }
808 815
809 /* Sets things up for a new user, like dgamelaunch's commands[register] */ 816 /* Sets things up for a new user, like dgamelaunch's commands[register] */
810 function regsetup(username) { 817 function regsetup(username) {
811 function regsetup_l2(err) { 818 function regsetup_l2(err) {
884 readFeed(client, res); 891 readFeed(client, res);
885 return; 892 return;
886 } 893 }
887 894
888 /* Stops a running game if the request has the proper key. */ 895 /* Stops a running game if the request has the proper key. */
896 /* TODO does this still work? */
889 function stopgame(res, formdata) { 897 function stopgame(res, formdata) {
890 if (!("key" in formdata) || !(formdata["key"] in logins)) { 898 if (!("key" in formdata) || !(formdata["key"] in logins)) {
891 sendError(res, 1); 899 sendError(res, 1);
892 return; 900 return;
893 } 901 }
930 }); 938 });
931 } 939 }
932 checkprogress(pname, games[gname], checkback, []); 940 checkprogress(pname, games[gname], checkback, []);
933 } 941 }
934 942
943 /* TODO remove polling */
935 function findClient(formdata, playersOnly) { 944 function findClient(formdata, playersOnly) {
936 if (typeof(formdata) != "object") 945 if (typeof(formdata) != "object")
937 return null; 946 return null;
938 if ("id" in formdata) { 947 if ("id" in formdata) {
939 var id = formdata["id"]; 948 var id = formdata["id"];
1021 } 1030 }
1022 }); 1031 });
1023 return; 1032 return;
1024 } 1033 }
1025 1034
1035 /* TODO remove polling */
1026 function readFeed(client, res) { 1036 function readFeed(client, res) {
1027 if (!client) { 1037 if (!client) {
1028 sendError(res, 7, null, true); 1038 sendError(res, 7, null, true);
1029 return; 1039 return;
1030 } 1040 }
1036 res.writeHead(200, { "Content-Type": "application/json" }); 1046 res.writeHead(200, { "Content-Type": "application/json" });
1037 res.write(JSON.stringify(msgs)); 1047 res.write(JSON.stringify(msgs));
1038 res.end(); 1048 res.end();
1039 } 1049 }
1040 1050
1051 /* TODO simplify by storing timestamps instead of callin stat() */
1041 function getStatus(callback) { 1052 function getStatus(callback) {
1042 var now = new Date(); 1053 var now = new Date();
1043 var statusinfo = {"s": allowlogin, "g": []}; 1054 var statusinfo = {"s": allowlogin, "g": []};
1044 function idleset(i, idletime) { 1055 function idleset(n, idletime) {
1045 if (i >= 0 && i < statusinfo.g.length) { 1056 if (n >= 0 && n < statusinfo.g.length) {
1046 statusinfo.g[i].i = idletime; 1057 statusinfo.g[n].i = idletime;
1047 } 1058 }
1048 for (var j = 0; j < statusinfo.g.length; j++) { 1059 for (var j = 0; j < statusinfo.g.length; j++) {
1049 if (!("i" in statusinfo.g[j])) 1060 if (!("i" in statusinfo.g[j]))
1050 return; 1061 return;
1051 } 1062 }
1052 callback(statusinfo); 1063 callback(statusinfo);
1053 } 1064 }
1054 for (var sessid in sessions) { 1065 for (var tag in sessions) {
1055 var gamedesc = {}; 1066 var gamedesc = {};
1056 gamedesc["n"] = sessid; 1067 gamedesc["tag"] = tag;
1057 gamedesc["p"] = sessions[sessid].pname; 1068 gamedesc["p"] = sessions[tag].pname;
1058 gamedesc["g"] = sessions[sessid].game.uname; 1069 gamedesc["g"] = sessions[tag].game.uname;
1059 statusinfo["g"].push(gamedesc); 1070 statusinfo["g"].push(gamedesc);
1060 } 1071 }
1061 statusinfo["dgl"] = []; 1072 statusinfo["dgl"] = [];
1062 for (var tag in dglgames) { 1073 for (var tag in dglgames) {
1063 statusinfo["dgl"].push(tag); 1074 statusinfo["dgl"].push(tag);
1074 idleset(i, now - stats.mtime); 1085 idleset(i, now - stats.mtime);
1075 } 1086 }
1076 } 1087 }
1077 for (var i = 0; i < statusinfo.g.length; i++) { 1088 for (var i = 0; i < statusinfo.g.length; i++) {
1078 /* fd sometimes isn't a number, presumably when the file isn't open yet. */ 1089 /* fd sometimes isn't a number, presumably when the file isn't open yet. */
1079 var ssid = statusinfo.g[i].n; 1090 /* FIXME sessid -> tag */
1080 if (ssid in sessions && typeof(sessions[ssid].record.fd) == 'number') { 1091 var tag = statusinfo.g[i].tag;
1081 fs.fstat(sessions[ssid].record.fd, makecallback(i)); 1092 if (tag in sessions && typeof(sessions[tag].record.fd) == 'number') {
1093 fs.fstat(sessions[tag].record.fd, makecallback(i));
1082 } 1094 }
1083 else { 1095 else {
1084 idleset(i, null); 1096 idleset(i, null);
1085 } 1097 }
1086 } 1098 }
1342 req.on('end', respond); 1354 req.on('end', respond);
1343 1355
1344 } 1356 }
1345 1357
1346 function wsHandler(wsRequest) { 1358 function wsHandler(wsRequest) {
1347 var watchmatch = wsRequest.resource.match(/^\/watch\/([0-9]*)$/); 1359 var watchmatch = wsRequest.resource.match(/^\/watch\/(.*)$/);
1348 var playmatch = wsRequest.resource.match(/^\/play\//); 1360 var playmatch = wsRequest.resource.match(/^\/play\//);
1349 if (watchmatch !== null) { 1361 if (watchmatch !== null) {
1350 if (watchmatch[1] && Number(watchmatch[1]) in sessions) { 1362 if (!(watchmatch[1] in sessions)) {
1351 var tsession = sessions[Number(watchmatch[1])];
1352 var conn = wsRequest.accept(null, wsRequest.origin);
1353 new wsWatcher(conn, tsession);
1354 tslog("Game %d is being watched via WebSockets", tsession.sessid);
1355 }
1356 else
1357 wsRequest.reject(404, errorcodes[7]); 1363 wsRequest.reject(404, errorcodes[7]);
1364 return;
1365 }
1366 var tsession = sessions[watchmatch[1]];
1367 var conn = wsRequest.accept(null, wsRequest.origin);
1368 new wsWatcher(conn, tsession);
1369 tslog("Game %d is being watched via WebSockets", tsession.tag());
1358 } 1370 }
1359 else if (playmatch !== null) { 1371 else if (playmatch !== null) {
1360 wsStart(wsRequest); 1372 wsStart(wsRequest);
1361 } 1373 }
1362 else if (wsRequest.resourceURL.pathname == "/status") { 1374 else if (wsRequest.resourceURL.pathname == "/status") {
1365 getStatus(function (info) { 1377 getStatus(function (info) {
1366 info["t"] = "t"; 1378 info["t"] = "t";
1367 conn.sendUTF(JSON.stringify(info)); 1379 conn.sendUTF(JSON.stringify(info));
1368 }); 1380 });
1369 } 1381 }
1370 var beginH = function (n, name, game) { 1382 var beginH = function (gname, pname) {
1371 conn.sendUTF(JSON.stringify({"t": "b", "n": n, "p": name, "g": game})); 1383 conn.sendUTF(JSON.stringify({"t": "b", "p": pname, "g": gname}));
1372 }; 1384 };
1373 var listH = function (list) { 1385 var listH = function (list) {
1374 conn.sendUTF(JSON.stringify(list)); 1386 conn.sendUTF(JSON.stringify(list));
1375 }; 1387 };
1376 var endH = function (n, pname, gname) { 1388 var endH = function (gname, pname) {
1377 conn.sendUTF(JSON.stringify({"t": "e", "n": n, "p": pname, "g": gname})); 1389 conn.sendUTF(JSON.stringify({"t": "e", "p": pname, "g": gname}));
1378 }; 1390 };
1379 gamemux.on('begin', beginH); 1391 gamemux.on('begin', beginH);
1380 gamemux.on('list', listH); 1392 gamemux.on('list', listH);
1381 gamemux.on('end', endH); 1393 gamemux.on('end', endH);
1382 conn.on('message', tell); 1394 conn.on('message', tell);
1404 ctlServer.close(); 1416 ctlServer.close();
1405 tslog("Shutting down..."); 1417 tslog("Shutting down...");
1406 process.exit(); 1418 process.exit();
1407 } 1419 }
1408 1420
1409 function conHandler(chunk) { 1421 function consoleHandler(chunk) {
1410 var msg = chunk.toString().split('\n')[0]; 1422 var msg = chunk.toString().split('\n')[0];
1411 if (msg == "quit") { 1423 if (msg == "quit") {
1412 allowlogin = false; 1424 allowlogin = false;
1413 tslog("Disconnecting..."); 1425 tslog("Disconnecting...");
1414 for (var sessid in sessions) { 1426 for (var tag in sessions) {
1415 sessions[sessid].close(); 1427 sessions[tag].close();
1416 } 1428 }
1417 progressWatcher.stdin.end("\n"); 1429 progressWatcher.stdin.end("\n");
1418 setTimeout(shutdown, 2000); 1430 setTimeout(shutdown, 2000);
1419 } 1431 }
1420 } 1432 }
1421 1433
1422 process.on("exit", function () { 1434 process.on("exit", function () {
1423 for (var sessid in sessions) { 1435 for (var tag in sessions) {
1424 sessions[sessid].term.kill('SIGHUP'); 1436 sessions[tag].term.kill('SIGHUP');
1425 } 1437 }
1426 tslog("Quitting..."); 1438 tslog("Quitting...");
1427 return; 1439 return;
1428 }); 1440 });
1429 1441
1453 fs.unlinkSync(ctlsocket); 1465 fs.unlinkSync(ctlsocket);
1454 } 1466 }
1455 1467
1456 /* Open the control socket before chrooting where it can't be found */ 1468 /* Open the control socket before chrooting where it can't be found */
1457 var ctlServer = net.createServer(function (sock) { 1469 var ctlServer = net.createServer(function (sock) {
1458 sock.on('data', conHandler); 1470 sock.on('data', consoleHandler);
1459 }); 1471 });
1460 ctlServer.listen(ctlsocket, function () { 1472 ctlServer.listen(ctlsocket, function () {
1461 /* rlgwebd.js now assumes that it has been started by the rlgwebd shell 1473 /* rlgwebd.js now assumes that it has been started by the rlgwebd shell
1462 * script, or some other method that detaches it and sets up stdio. */ 1474 * script, or some other method that detaches it and sets up stdio. */
1463 /* chroot and drop permissions. posix.chroot() does chdir() itself. */ 1475 /* chroot and drop permissions. posix.chroot() does chdir() itself. */