comparison rlgwebd.js @ 184:ecedc6f7e4ac

Move TermSession and DglSession common code into a base class.
author John "Elwin" Edwards
date Mon, 19 Jan 2015 14:44:27 -0500
parents db2f5ab112e9
children bbfda4a4eb7f
comparison
equal deleted inserted replaced
183:db2f5ab112e9 184:ecedc6f7e4ac
64 var sessions = {}; 64 var sessions = {};
65 var dglgames = {}; 65 var dglgames = {};
66 var allowlogin = true; 66 var allowlogin = true;
67 var gamemux = new events.EventEmitter(); 67 var gamemux = new events.EventEmitter();
68 68
69 /* TODO move TermSession and DglSession methods into the prototypes. */ 69 /* A base class. TermSession and DglSession inherit from it. */
70 function BaseGame() {
71 /* Games subclass EventEmitter, though there are few listeners. */
72 events.EventEmitter.call(this);
73 /* Array of watching WebSockets. */
74 this.watchers = [];
75 /* replaybuf holds the output since the last screen clear, so watchers can
76 * begin with a complete screen. replaylen is the number of bytes stored. */
77 this.replaybuf = new Buffer(1024);
78 this.replaylen = 0;
79 /* Time of last activity. */
80 this.lasttime = new Date();
81 }
82 BaseGame.prototype = new events.EventEmitter();
83
84 BaseGame.prototype.tag = function () {
85 if (this.pname === undefined || this.gname === undefined)
86 return "";
87 return this.gname + "/" + this.pname;
88 };
89
90 BaseGame.prototype.framepush = function(chunk) {
91 /* If this chunk resets the screen, discard what preceded it. */
92 if (isclear(chunk)) {
93 this.replaybuf = new Buffer(1024);
94 this.replaylen = 0;
95 }
96 /* Make sure there's space. */
97 while (this.replaybuf.length < chunk.length + this.replaylen) {
98 var nbuf = new Buffer(this.replaybuf.length * 2);
99 this.replaybuf.copy(nbuf, 0, 0, this.replaylen);
100 this.replaybuf = nbuf;
101 if (this.replaybuf.length > 65536) {
102 tslog("Warning: %s frame buffer at %d bytes", this.tag(),
103 this.replaybuf.length);
104 }
105 }
106 chunk.copy(this.replaybuf, this.replaylen);
107 this.replaylen += chunk.length;
108 };
109
110 /* Adds a watcher. */
111 BaseGame.prototype.attach = function (wsReq) {
112 var conn = wsReq.accept(null, wsReq.origin);
113 conn.sendUTF(JSON.stringify({
114 "t": "w", "w": this.w, "h": this.h, "p": this.pname, "g": this.gname
115 }));
116 conn.sendUTF(JSON.stringify({"t": "d",
117 "d": this.replaybuf.toString("hex", 0, this.replaylen)}));
118 conn.on('close', this.detach.bind(this, conn));
119 this.watchers.push(conn);
120 };
121
122 BaseGame.prototype.detach = function (socket) {
123 var n = this.watchers.indexOf(socket);
124 if (n >= 0) {
125 this.watchers.splice(n, 1);
126 tslog("A WebSocket watcher has left game %s", this.tag());
127 }
128 };
70 129
71 /* Constructor. A TermSession handles a pty and the game running on it. 130 /* Constructor. A TermSession handles a pty and the game running on it.
72 * gname: (String) Name of the game to launch. 131 * gname: (String) Name of the game to launch.
73 * pname: (String) The player's name. 132 * pname: (String) The player's name.
74 * wsReq: (WebSocketRequest) The request from the client. 133 * wsReq: (WebSocketRequest) The request from the client.
76 * Events: 135 * Events:
77 * "data": Data generated by child. Parameters: buf (Buffer) 136 * "data": Data generated by child. Parameters: buf (Buffer)
78 * "exit": Child terminated. Parameters: none 137 * "exit": Child terminated. Parameters: none
79 */ 138 */
80 function TermSession(gname, pname, wsReq) { 139 function TermSession(gname, pname, wsReq) {
81 /* Subclass EventEmitter to do the hard work. */ 140 BaseGame.call(this);
82 events.EventEmitter.call(this);
83 /* Don't launch anything that's not a real game. */ 141 /* Don't launch anything that's not a real game. */
84 if (gname in games) { 142 if (gname in games) {
85 this.game = games[gname]; 143 this.game = games[gname];
86 this.gname = gname; 144 this.gname = gname;
87 } 145 }
111 tslog("%s playing %s (pid %d)", this.pname, this.gname, this.term.pid); 169 tslog("%s playing %s (pid %d)", this.pname, this.gname, this.term.pid);
112 this.failed = false; 170 this.failed = false;
113 sessions[this.gname + "/" + this.pname] = this; 171 sessions[this.gname + "/" + this.pname] = this;
114 gamemux.emit('begin', this.gname, this.pname, 'rlg'); 172 gamemux.emit('begin', this.gname, this.pname, 'rlg');
115 /* Set up the lockfile and ttyrec */ 173 /* Set up the lockfile and ttyrec */
116 this.lasttime = new Date();
117 var ts = timestamp(this.lasttime); 174 var ts = timestamp(this.lasttime);
118 var progressdir = path.join("/dgldir/inprogress", this.gname); 175 var progressdir = path.join("/dgldir/inprogress", this.gname);
119 this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec"); 176 this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec");
120 var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n'; 177 var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n';
121 fs.writeFile(this.lock, lmsg, "utf8"); 178 fs.writeFile(this.lock, lmsg, "utf8");
122 var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, 179 var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname,
123 ts + ".ttyrec"); 180 ts + ".ttyrec");
124 this.record = fs.createWriteStream(ttyrec, { mode: 0664 }); 181 this.record = fs.createWriteStream(ttyrec, { mode: 0664 });
125 /* Holds the output since the last screen clear, so watchers can begin
126 * with a complete screen. */
127 this.framebuf = new Buffer(1024);
128 this.frameoff = 0;
129 /* The player's WebSocket and its handlers. */ 182 /* The player's WebSocket and its handlers. */
130 this.playerconn = wsReq.accept(null, wsReq.origin); 183 this.playerconn = wsReq.accept(null, wsReq.origin);
131 this.playerconn.on('message', this.input_msg.bind(this)); 184 this.playerconn.on('message', this.input_msg.bind(this));
132 this.playerconn.on('close', this.close.bind(this)); 185 this.playerconn.on('close', this.close.bind(this));
133 /* Array for watcher connections. */
134 this.watchers = [];
135 /* Send initial data. */ 186 /* Send initial data. */
136 this.playerconn.sendUTF(JSON.stringify({"t": "s", "w": this.w, "h": this.h, 187 this.playerconn.sendUTF(JSON.stringify({"t": "s", "w": this.w, "h": this.h,
137 "p": this.pname, "g": this.gname})); 188 "p": this.pname, "g": this.gname}));
138 /* Begin the ttyrec with some metadata, like dgamelaunch does. */ 189 /* Begin the ttyrec with some metadata, like dgamelaunch does. */
139 var descstr = "\x1b[2J\x1b[1;1H\r\n"; 190 var descstr = "\x1b[2J\x1b[1;1H\r\n";
145 descstr += "Size: " + this.w + "x" + this.h + "\r\n\x1b[2J"; 196 descstr += "Size: " + this.w + "x" + this.h + "\r\n\x1b[2J";
146 this.write_ttyrec(descstr); 197 this.write_ttyrec(descstr);
147 this.term.on("data", this.write_ttyrec.bind(this)); 198 this.term.on("data", this.write_ttyrec.bind(this));
148 this.term.on("exit", this.destroy.bind(this)); 199 this.term.on("exit", this.destroy.bind(this));
149 } 200 }
150 TermSession.prototype = new events.EventEmitter(); 201 TermSession.prototype = new BaseGame();
151
152 TermSession.prototype.tag = function () {
153 if (this.pname === undefined || this.gname === undefined)
154 return "";
155 return this.gname + "/" + this.pname;
156 };
157
158 TermSession.prototype.framepush = function(chunk) {
159 /* If this chunk resets the screen, discard what preceded it. */
160 if (isclear(chunk)) {
161 this.framebuf = new Buffer(1024);
162 this.frameoff = 0;
163 }
164 /* Make sure there's space. */
165 while (this.framebuf.length < chunk.length + this.frameoff) {
166 var nbuf = new Buffer(this.framebuf.length * 2);
167 this.framebuf.copy(nbuf, 0, 0, this.frameoff);
168 this.framebuf = nbuf;
169 if (this.framebuf.length > 65536) {
170 tslog("Warning: Game %s frame buffer at %d bytes", this.tag(),
171 this.framebuf.length);
172 }
173 }
174 chunk.copy(this.framebuf, this.frameoff);
175 this.frameoff += chunk.length;
176 };
177 202
178 /* Currently this also sends to the player and any watchers. */ 203 /* Currently this also sends to the player and any watchers. */
179 TermSession.prototype.write_ttyrec = function (datastr) { 204 TermSession.prototype.write_ttyrec = function (datastr) {
180 this.lasttime = new Date(); 205 this.lasttime = new Date();
181 var buf = new Buffer(datastr); 206 var buf = new Buffer(datastr);
242 gamemux.emit('end', this.gname, this.pname); 267 gamemux.emit('end', this.gname, this.pname);
243 delete sessions[tag]; 268 delete sessions[tag];
244 tslog("Game %s ended.", tag); 269 tslog("Game %s ended.", tag);
245 }; 270 };
246 271
247 /* Adds a watcher. */
248 TermSession.prototype.attach = function (wsReq) {
249 var conn = wsReq.accept(null, wsReq.origin);
250 conn.sendUTF(JSON.stringify({
251 "t": "w", "w": this.w, "h": this.h, "p": this.pname, "g": this.gname
252 }));
253 conn.sendUTF(JSON.stringify({"t": "d",
254 "d": this.framebuf.toString("hex", 0, this.frameoff)}));
255 conn.on('close', this.detach.bind(this, conn));
256 this.watchers.push(conn);
257 };
258
259 TermSession.prototype.detach = function (socket) {
260 var n = this.watchers.indexOf(socket);
261 if (n >= 0) {
262 this.watchers.splice(n, 1);
263 tslog("A WebSocket watcher has left game %s", this.tag());
264 }
265 };
266
267 function DglSession(filename) { 272 function DglSession(filename) {
268 var ss = this; 273 var ss = this;
269 events.EventEmitter.call(this); 274 BaseGame.call(this);
270 var pathcoms = filename.split('/'); 275 var pathcoms = filename.split('/');
271 this.gname = pathcoms[pathcoms.length - 2]; 276 this.gname = pathcoms[pathcoms.length - 2];
272 if (!(this.gname in games)) { 277 if (!(this.gname in games)) {
273 ss.emit('open', false); 278 ss.emit('open', false);
274 return; 279 return;
280 this.ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, fname); 285 this.ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, fname);
281 /* Flag to prevent multiple handlers from reading simultaneously and 286 /* Flag to prevent multiple handlers from reading simultaneously and
282 * getting into a race. */ 287 * getting into a race. */
283 this.reading = false; 288 this.reading = false;
284 this.rpos = 0; 289 this.rpos = 0;
285 this.framebuf = new Buffer(1024);
286 this.frameoff = 0;
287 this.framepush = function(chunk) {
288 /* If this chunk resets the screen, discard what preceded it. */
289 if (isclear(chunk)) {
290 this.framebuf = new Buffer(1024);
291 this.frameoff = 0;
292 }
293 /* Make sure there's space. */
294 while (this.framebuf.length < chunk.length + this.frameoff) {
295 var nbuf = new Buffer(this.framebuf.length * 2);
296 this.framebuf.copy(nbuf, 0, 0, this.frameoff);
297 this.framebuf = nbuf;
298 if (this.framebuf.length > 65536) {
299 tslog("Warning: DGL %s frame buffer at %d bytes", this.tag(),
300 this.framebuf.length);
301 }
302 }
303 chunk.copy(this.framebuf, this.frameoff);
304 this.frameoff += chunk.length;
305 };
306 this.readchunk = function () { 290 this.readchunk = function () {
307 if (this.reading) 291 if (this.reading)
308 return; 292 return;
309 this.reading = true; 293 this.reading = true;
310 var header = new Buffer(12); 294 var header = new Buffer(12);
373 ss.readchunk(); 357 ss.readchunk();
374 }); 358 });
375 } 359 }
376 }); 360 });
377 }); 361 });
378 this.tag = function () {
379 return this.gname + "/" + this.pname;
380 };
381 this.close = function () { 362 this.close = function () {
382 this.recwatcher.close() 363 this.recwatcher.close()
383 /* Ensure all data is handled before quitting. */ 364 /* Ensure all data is handled before quitting. */
384 this.readchunk(); 365 this.readchunk();
385 var connlist = this.watchers; 366 var connlist = this.watchers;
391 fs.close(this.fd); 372 fs.close(this.fd);
392 this.emit("close"); 373 this.emit("close");
393 gamemux.emit('end', this.gname, this.pname); 374 gamemux.emit('end', this.gname, this.pname);
394 tslog("DGL %s: closed", ss.tag()); 375 tslog("DGL %s: closed", ss.tag());
395 }; 376 };
396 this.watchers = []; 377 }
397 /* Attach a watcher. */ 378 DglSession.prototype = new BaseGame();
398 this.attach = function (wsReq) {
399 var conn = wsReq.accept(null, wsReq.origin);
400 conn.sendUTF(JSON.stringify({
401 "t": "w", "w": this.w, "h": this.h, "p": this.pname,
402 "g": this.gname
403 }));
404 conn.sendUTF(JSON.stringify({"t": "d",
405 "d": this.framebuf.toString("hex", 0, this.frameoff)}));
406 conn.on('close', function () {
407 /* 'this' is the connection when triggered */
408 var n = ss.watchers.indexOf(this);
409 if (n >= 0) {
410 ss.watchers.splice(n, 1);
411 tslog("A WebSocket watcher has left DGL game %s", ss.tag());
412 }
413 });
414 this.watchers.push(conn);
415 };
416 this.lasttime = new Date();
417 }
418 DglSession.prototype = new events.EventEmitter();
419 379
420 function wsStartGame(wsReq) { 380 function wsStartGame(wsReq) {
421 var playmatch = wsReq.resourceURL.pathname.match(/^\/play\/([^\/]*)$/); 381 var playmatch = wsReq.resourceURL.pathname.match(/^\/play\/([^\/]*)$/);
422 if (!playmatch[1] || !(playmatch[1] in games)) { 382 if (!playmatch[1] || !(playmatch[1] in games)) {
423 wsReq.reject(404, errorcodes[2]); 383 wsReq.reject(404, errorcodes[2]);