Mercurial > hg > rlgwebd
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]); |