comparison rlgwebd.js @ 170:50e4c9feeac2

RLGWebD: fix simultaneous player bug. Multiple games can now run at the same time, and data will be sent to the proper place. The interaction of multiple players with watchers has not yet been tested.
author John "Elwin" Edwards
date Fri, 09 Jan 2015 13:06:41 -0500
parents 6f4b7e1b32e8
children 671bed5039aa
comparison
equal deleted inserted replaced
169:6f4b7e1b32e8 170:50e4c9feeac2
67 var gamemux = new events.EventEmitter(); 67 var gamemux = new events.EventEmitter();
68 68
69 /* Constructor. A TermSession handles a pty and the game running on it. 69 /* Constructor. A TermSession handles a pty and the game running on it.
70 * gname: (String) Name of the game to launch. 70 * gname: (String) Name of the game to launch.
71 * pname: (String) The player's name. 71 * pname: (String) The player's name.
72 * dims: (Array [Number, Number]) Height and width of the pty. 72 * wsReq: (WebSocketRequest) The request from the client.
73 * handlers: (Object) Key-value pairs, event names and functions to 73 *
74 * install to handle them.
75 * Events: 74 * Events:
76 * "data": Data generated by child. Parameters: buf (Buffer) 75 * "data": Data generated by child. Parameters: buf (Buffer)
77 * "exit": Child terminated. Parameters: none 76 * "exit": Child terminated. Parameters: none
78 */ 77 */
79 function TermSession(gname, pname, dims, handlers) { 78 function TermSession(gname, pname, wsReq) {
80 var ss = this; 79 var ss = this;
81 /* Subclass EventEmitter to do the hard work. */ 80 /* Subclass EventEmitter to do the hard work. */
82 events.EventEmitter.call(this); 81 events.EventEmitter.call(this);
83 for (var evname in handlers)
84 this.on(evname, handlers[evname]);
85 /* Don't launch anything that's not a real game. */ 82 /* Don't launch anything that's not a real game. */
86 if (gname in games) { 83 if (gname in games) {
87 this.game = games[gname]; 84 this.game = games[gname];
88 } 85 }
89 else { 86 else {
90 this.failed = true; 87 this.failed = true;
88 wsReq.reject(404, errorcodes[2], "No such game");
89 tslog("Game %s is not available", game);
91 return; 90 return;
92 } 91 }
93 this.pname = pname; 92 this.pname = pname;
94 /* Grab a spot in the sessions table. */
95 sessions[this.game.uname + "/" + this.pname] = this;
96 /* Set up the sizes. */ 93 /* Set up the sizes. */
97 this.w = Math.floor(Number(dims[1])); 94 this.w = Math.floor(Number(wsReq.resourceURL.query.w));
98 if (!(this.w > 0 && this.w < 256)) 95 if (!(this.w > 0 && this.w < 256))
99 this.w = 80; 96 this.w = 80;
100 this.h = Math.floor(Number(dims[0])); 97 this.h = Math.floor(Number(wsReq.resourceURL.query.h));
101 if (!(this.h > 0 && this.h < 256)) 98 if (!(this.h > 0 && this.h < 256))
102 this.h = 24; 99 this.h = 24;
103 /* Environment. */ 100 /* Environment. */
104 var childenv = {}; 101 var childenv = {};
105 for (var key in process.env) { 102 for (var key in process.env) {
109 var spawnopts = {"env": childenv, "cwd": "/", "rows": this.h, "cols": this.w, 106 var spawnopts = {"env": childenv, "cwd": "/", "rows": this.h, "cols": this.w,
110 "name": "xterm-256color"}; 107 "name": "xterm-256color"};
111 this.term = pty.spawn(this.game.path, args, spawnopts); 108 this.term = pty.spawn(this.game.path, args, spawnopts);
112 tslog("%s playing %s (pid %d)", this.pname, this.game.uname, this.term.pid); 109 tslog("%s playing %s (pid %d)", this.pname, this.game.uname, this.term.pid);
113 this.failed = false; 110 this.failed = false;
111 sessions[this.game.uname + "/" + this.pname] = this;
114 gamemux.emit('begin', this.game.uname, this.pname); 112 gamemux.emit('begin', this.game.uname, this.pname);
115 /* Set up the lockfile and ttyrec */ 113 /* Set up the lockfile and ttyrec */
116 this.lasttime = new Date(); 114 this.lasttime = new Date();
117 var ts = timestamp(this.lasttime); 115 var ts = timestamp(this.lasttime);
118 var progressdir = path.join("/dgldir/inprogress", this.game.uname); 116 var progressdir = path.join("/dgldir/inprogress", this.game.uname);
124 this.record = fs.createWriteStream(ttyrec, { mode: 0664 }); 122 this.record = fs.createWriteStream(ttyrec, { mode: 0664 });
125 /* Holds the output since the last screen clear, so watchers can begin 123 /* Holds the output since the last screen clear, so watchers can begin
126 * with a complete screen. */ 124 * with a complete screen. */
127 this.framebuf = new Buffer(1024); 125 this.framebuf = new Buffer(1024);
128 this.frameoff = 0; 126 this.frameoff = 0;
127 this.playerconn = wsReq.accept(null, wsReq.origin);
129 /* END setup */ 128 /* END setup */
130 function ttyrec_chunk(datastr) { 129 function ttyrec_chunk(datastr) {
131 ss.lasttime = new Date(); 130 ss.lasttime = new Date();
132 var buf = new Buffer(datastr); 131 var buf = new Buffer(datastr);
133 var chunk = new Buffer(buf.length + 12); 132 var chunk = new Buffer(buf.length + 12);
136 chunk.writeUInt32LE(1000 * (ss.lasttime.getTime() % 1000), 4); 135 chunk.writeUInt32LE(1000 * (ss.lasttime.getTime() % 1000), 4);
137 chunk.writeUInt32LE(buf.length, 8); 136 chunk.writeUInt32LE(buf.length, 8);
138 buf.copy(chunk, 12); 137 buf.copy(chunk, 12);
139 ss.record.write(chunk); 138 ss.record.write(chunk);
140 ss.framepush(buf); 139 ss.framepush(buf);
140 /* Send to the player. */
141 var msg = {"t": "d", "d": buf.toString("hex")};
142 ss.playerconn.sendUTF(JSON.stringify(msg));
143 /* For the benefit of watchers. */
141 ss.emit('data', buf); 144 ss.emit('data', buf);
142 } 145 }
143 this.term.on("data", ttyrec_chunk); 146 this.term.on("data", ttyrec_chunk);
144 this.framepush = function(chunk) { 147 this.framepush = function(chunk) {
145 /* If this chunk resets the screen, discard what preceded it. */ 148 /* If this chunk resets the screen, discard what preceded it. */
169 // Teardown. 172 // Teardown.
170 this.term.on("exit", function () { 173 this.term.on("exit", function () {
171 var tag = ss.tag(); 174 var tag = ss.tag();
172 fs.unlink(ss.lock); 175 fs.unlink(ss.lock);
173 ss.record.end(); 176 ss.record.end();
177 if (ss.playerconn.connected) {
178 ss.playerconn.sendUTF(JSON.stringify({"t": "q"}));
179 ss.playerconn.close();
180 }
174 ss.emit('exit'); 181 ss.emit('exit');
175 gamemux.emit('end', ss.game.uname, ss.pname); 182 gamemux.emit('end', ss.game.uname, ss.pname);
176 delete sessions[tag]; 183 delete sessions[tag];
177 tslog("Game %s ended.", tag); 184 tslog("Game %s ended.", tag);
178 }); 185 });
179 this.close = function () { 186 this.close = function () {
180 if (this.tag() in sessions) 187 if (ss.tag() in sessions)
181 this.term.kill('SIGHUP'); 188 ss.term.kill('SIGHUP');
182 }; 189 };
190 /* Send initial data. */
191 this.playerconn.sendUTF(JSON.stringify({"t": "s", "w": this.w, "h": this.h,
192 "p": this.pname, "g": this.game.uname}));
193 /* Attach handlers. */
194 function messageH(message) {
195 var parsedMsg = getMsgWS(message);
196 if (parsedMsg.t == 'q') {
197 ss.close();
198 }
199 else if (parsedMsg.t == 'd') {
200 var hexstr = parsedMsg.d.replace(/[^0-9a-f]/gi, "");
201 if (hexstr.length % 2 != 0) {
202 hexstr = hexstr.slice(0, -1);
203 }
204 var keybuf = new Buffer(hexstr, "hex");
205 ss.write(keybuf);
206 }
207 }
208 this.playerconn.on('message', messageH);
209 this.playerconn.on('close', this.close);
183 } 210 }
184 TermSession.prototype = new events.EventEmitter(); 211 TermSession.prototype = new events.EventEmitter();
185 212
186 function DglSession(filename) { 213 function DglSession(filename) {
187 var ss = this; 214 var ss = this;
313 })); 340 }));
314 conn.sendUTF(JSON.stringify({"t": "d", 341 conn.sendUTF(JSON.stringify({"t": "d",
315 "d": session.framebuf.toString("hex", 0, session.frameoff)})); 342 "d": session.framebuf.toString("hex", 0, session.frameoff)}));
316 } 343 }
317 344
318 function wsPlay(wsReq, game, pname, dims) { 345 function wsStartGame(wsReq) {
319 tslog("wsPlay: running for %s/%s", game, pname);
320 tslog("Request is for %s", logins[wsReq.resourceURL.query["key"]].name);
321 var conn;
322 var session;
323 /* Listeners on the WebSocket */
324 function messageH(message) {
325 var parsedMsg = getMsgWS(message);
326 if (parsedMsg.t == 'q') {
327 session.close();
328 }
329 else if (parsedMsg.t == 'd') {
330 var hexstr = parsedMsg.d.replace(/[^0-9a-f]/gi, "");
331 if (hexstr.length % 2 != 0) {
332 hexstr = hexstr.slice(0, -1);
333 }
334 var keybuf = new Buffer(hexstr, "hex");
335 session.write(keybuf);
336 }
337 }
338 function closeH() {
339 session.close();
340 }
341 /* These listen on the TermSession. */
342 function dataH(chunk) {
343 var msg = {};
344 msg.t = "d";
345 msg.d = chunk.toString("hex");
346 conn.sendUTF(JSON.stringify(msg));
347 }
348 function exitH() {
349 if (conn.connected)
350 conn.sendUTF(JSON.stringify({"t": "q"}));
351 conn.close();
352 session.removeListener('data', dataH);
353 session.removeListener('exit', exitH);
354 }
355 var handlers = {'data': dataH, 'exit': exitH};
356 session = new TermSession(game, pname, dims, handlers);
357 if (!session.failed) {
358 var tag = session.game.uname + "/" + session.pname;
359 var reply = {"t": "s", "tag": tag, "w": session.w, "h": session.h,
360 "p": session.pname, "g": session.game.uname};
361 tslog("Accepting for %s", tag);
362 tslog("Request is for %s", logins[wsReq.resourceURL.query["key"]].name);
363 tslog("Session is for %s", session.pname);
364 conn = wsReq.accept(null, wsReq.origin);
365 conn.sendUTF(JSON.stringify(reply));
366 conn.on('message', messageH);
367 conn.on('close', closeH);
368 }
369 else {
370 wsReq.reject(500, errorcodes[5]);
371 tslog("Unable to allocate TTY for %s", game);
372 }
373 }
374
375 function wsStart(wsReq) {
376 var playmatch = wsReq.resourceURL.pathname.match(/^\/play\/([^\/]*)$/); 346 var playmatch = wsReq.resourceURL.pathname.match(/^\/play\/([^\/]*)$/);
377 if (!playmatch[1] || !(playmatch[1] in games)) { 347 if (!playmatch[1] || !(playmatch[1] in games)) {
378 wsReq.reject(404, errorcodes[2]); 348 wsReq.reject(404, errorcodes[2]);
379 return; 349 return;
380 } 350 }
391 if (!(lkey in logins)) { 361 if (!(lkey in logins)) {
392 wsReq.reject(404, errorcodes[1]); 362 wsReq.reject(404, errorcodes[1]);
393 return; 363 return;
394 } 364 }
395 var pname = logins[lkey].name; 365 var pname = logins[lkey].name;
396 var dims = [wsReq.resourceURL.query.h, wsReq.resourceURL.query.w];
397 function progcallback(err, fname) { 366 function progcallback(err, fname) {
398 if (fname) { 367 if (fname) {
399 wsReq.reject(404, errorcodes[4]); 368 wsReq.reject(404, errorcodes[4]);
400 tslog("%s is already playing %s", pname, gname); 369 tslog("%s is already playing %s", pname, gname);
401 } 370 }
402 else 371 else {
403 wsPlay(wsReq, gname, pname, dims); 372 new TermSession(gname, pname, wsReq);
373 }
404 }; 374 };
405 checkprogress(pname, games[gname], progcallback, []); 375 checkprogress(pname, games[gname], progcallback, []);
406 } 376 }
407 377
408 /* Some functions which check whether a player is currently playing or 378 /* Some functions which check whether a player is currently playing or
1042 var conn = wsRequest.accept(null, wsRequest.origin); 1012 var conn = wsRequest.accept(null, wsRequest.origin);
1043 new wsWatcher(conn, tsession); 1013 new wsWatcher(conn, tsession);
1044 tslog("Game %s is being watched via WebSockets", tsession.tag()); 1014 tslog("Game %s is being watched via WebSockets", tsession.tag());
1045 } 1015 }
1046 else if (playmatch !== null) { 1016 else if (playmatch !== null) {
1047 wsStart(wsRequest); 1017 wsStartGame(wsRequest);
1048 } 1018 }
1049 else if (wsRequest.resourceURL.pathname == "/status") { 1019 else if (wsRequest.resourceURL.pathname == "/status") {
1050 var conn = wsRequest.accept(null, wsRequest.origin); 1020 var conn = wsRequest.accept(null, wsRequest.origin);
1051 var tell = function () { 1021 var tell = function () {
1052 getStatus(function (info) { 1022 getStatus(function (info) {