Mercurial > hg > rlgwebd
comparison rlgwebd.js @ 183:db2f5ab112e9
Move all TermSession methods into the prototype.
| author | John "Elwin" Edwards |
|---|---|
| date | Mon, 19 Jan 2015 08:32:29 -0500 |
| parents | 3c0e7697bb30 |
| children | ecedc6f7e4ac |
comparison
equal
deleted
inserted
replaced
| 182:3c0e7697bb30 | 183:db2f5ab112e9 |
|---|---|
| 76 * Events: | 76 * Events: |
| 77 * "data": Data generated by child. Parameters: buf (Buffer) | 77 * "data": Data generated by child. Parameters: buf (Buffer) |
| 78 * "exit": Child terminated. Parameters: none | 78 * "exit": Child terminated. Parameters: none |
| 79 */ | 79 */ |
| 80 function TermSession(gname, pname, wsReq) { | 80 function TermSession(gname, pname, wsReq) { |
| 81 var ss = this; | |
| 82 /* Subclass EventEmitter to do the hard work. */ | 81 /* Subclass EventEmitter to do the hard work. */ |
| 83 events.EventEmitter.call(this); | 82 events.EventEmitter.call(this); |
| 84 /* Don't launch anything that's not a real game. */ | 83 /* Don't launch anything that's not a real game. */ |
| 85 if (gname in games) { | 84 if (gname in games) { |
| 86 this.game = games[gname]; | 85 this.game = games[gname]; |
| 86 this.gname = gname; | |
| 87 } | 87 } |
| 88 else { | 88 else { |
| 89 this.failed = true; | 89 this.failed = true; |
| 90 wsReq.reject(404, errorcodes[2], "No such game"); | 90 wsReq.reject(404, errorcodes[2], "No such game"); |
| 91 tslog("Game %s is not available", game); | 91 tslog("Game %s is not available", game); |
| 106 } | 106 } |
| 107 var args = ["-n", this.pname]; | 107 var args = ["-n", this.pname]; |
| 108 var spawnopts = {"env": childenv, "cwd": "/", "rows": this.h, "cols": this.w, | 108 var spawnopts = {"env": childenv, "cwd": "/", "rows": this.h, "cols": this.w, |
| 109 "name": "xterm-256color"}; | 109 "name": "xterm-256color"}; |
| 110 this.term = pty.spawn(this.game.path, args, spawnopts); | 110 this.term = pty.spawn(this.game.path, args, spawnopts); |
| 111 tslog("%s playing %s (pid %d)", this.pname, this.game.uname, this.term.pid); | 111 tslog("%s playing %s (pid %d)", this.pname, this.gname, this.term.pid); |
| 112 this.failed = false; | 112 this.failed = false; |
| 113 sessions[this.game.uname + "/" + this.pname] = this; | 113 sessions[this.gname + "/" + this.pname] = this; |
| 114 gamemux.emit('begin', this.game.uname, this.pname, 'rlg'); | 114 gamemux.emit('begin', this.gname, this.pname, 'rlg'); |
| 115 /* Set up the lockfile and ttyrec */ | 115 /* Set up the lockfile and ttyrec */ |
| 116 this.lasttime = new Date(); | 116 this.lasttime = new Date(); |
| 117 var ts = timestamp(this.lasttime); | 117 var ts = timestamp(this.lasttime); |
| 118 var progressdir = path.join("/dgldir/inprogress", this.game.uname); | 118 var progressdir = path.join("/dgldir/inprogress", this.gname); |
| 119 this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec"); | 119 this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec"); |
| 120 var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n'; | 120 var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n'; |
| 121 fs.writeFile(this.lock, lmsg, "utf8"); | 121 fs.writeFile(this.lock, lmsg, "utf8"); |
| 122 var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.game.uname, | 122 var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, |
| 123 ts + ".ttyrec"); | 123 ts + ".ttyrec"); |
| 124 this.record = fs.createWriteStream(ttyrec, { mode: 0664 }); | 124 this.record = fs.createWriteStream(ttyrec, { mode: 0664 }); |
| 125 /* Holds the output since the last screen clear, so watchers can begin | 125 /* Holds the output since the last screen clear, so watchers can begin |
| 126 * with a complete screen. */ | 126 * with a complete screen. */ |
| 127 this.framebuf = new Buffer(1024); | 127 this.framebuf = new Buffer(1024); |
| 128 this.frameoff = 0; | 128 this.frameoff = 0; |
| 129 /* The player's WebSocket and its handlers. */ | |
| 129 this.playerconn = wsReq.accept(null, wsReq.origin); | 130 this.playerconn = wsReq.accept(null, wsReq.origin); |
| 131 this.playerconn.on('message', this.input_msg.bind(this)); | |
| 132 this.playerconn.on('close', this.close.bind(this)); | |
| 130 /* Array for watcher connections. */ | 133 /* Array for watcher connections. */ |
| 131 this.watchers = []; | 134 this.watchers = []; |
| 132 /* END setup */ | 135 /* Send initial data. */ |
| 133 function ttyrec_chunk(datastr) { | 136 this.playerconn.sendUTF(JSON.stringify({"t": "s", "w": this.w, "h": this.h, |
| 134 ss.lasttime = new Date(); | 137 "p": this.pname, "g": this.gname})); |
| 135 var buf = new Buffer(datastr); | |
| 136 var chunk = new Buffer(buf.length + 12); | |
| 137 /* TTYREC headers */ | |
| 138 chunk.writeUInt32LE(Math.floor(ss.lasttime.getTime() / 1000), 0); | |
| 139 chunk.writeUInt32LE(1000 * (ss.lasttime.getTime() % 1000), 4); | |
| 140 chunk.writeUInt32LE(buf.length, 8); | |
| 141 buf.copy(chunk, 12); | |
| 142 ss.record.write(chunk); | |
| 143 ss.framepush(buf); | |
| 144 /* Send to the player. */ | |
| 145 var msg = JSON.stringify({"t": "d", "d": buf.toString("hex")}); | |
| 146 ss.playerconn.sendUTF(msg); | |
| 147 /* Send to any watchers. */ | |
| 148 for (var i = 0; i < ss.watchers.length; i++) { | |
| 149 if (ss.watchers[i].connected) | |
| 150 ss.watchers[i].sendUTF(msg); | |
| 151 } | |
| 152 ss.emit('data', buf); | |
| 153 } | |
| 154 /* Begin the ttyrec with some metadata, like dgamelaunch does. */ | 138 /* Begin the ttyrec with some metadata, like dgamelaunch does. */ |
| 155 var descstr = "\x1b[2J\x1b[1;1H\r\n"; | 139 var descstr = "\x1b[2J\x1b[1;1H\r\n"; |
| 156 descstr += "Player: " + this.pname + "\r\nGame: " + this.game.name + "\r\n"; | 140 descstr += "Player: " + this.pname + "\r\nGame: " + this.game.name + "\r\n"; |
| 157 descstr += "Server: Roguelike Gallery - rlgallery.org\r\n"; | 141 descstr += "Server: Roguelike Gallery - rlgallery.org\r\n"; |
| 158 descstr += "Filename: " + ts + ".ttyrec\r\n"; | 142 descstr += "Filename: " + ts + ".ttyrec\r\n"; |
| 159 descstr += "Time: (" + Math.floor(this.lasttime.getTime() / 1000) + ") "; | 143 descstr += "Time: (" + Math.floor(this.lasttime.getTime() / 1000) + ") "; |
| 160 descstr += this.lasttime.toUTCString().slice(0, -4) + "\r\n"; | 144 descstr += this.lasttime.toUTCString().slice(0, -4) + "\r\n"; |
| 161 descstr += "Size: " + this.w + "x" + this.h + "\r\n\x1b[2J"; | 145 descstr += "Size: " + this.w + "x" + this.h + "\r\n\x1b[2J"; |
| 162 ttyrec_chunk(descstr); | 146 this.write_ttyrec(descstr); |
| 163 this.term.on("data", ttyrec_chunk); | 147 this.term.on("data", this.write_ttyrec.bind(this)); |
| 164 this.write = function(data) { | 148 this.term.on("exit", this.destroy.bind(this)); |
| 165 this.term.write(data); | |
| 166 }; | |
| 167 // Teardown. | |
| 168 this.term.on("exit", function () { | |
| 169 var tag = ss.tag(); | |
| 170 fs.unlink(ss.lock); | |
| 171 ss.record.end(); | |
| 172 var watchsocks = ss.watchers; | |
| 173 ss.watchers = []; | |
| 174 for (var i = 0; i < watchsocks.length; i++) { | |
| 175 if (watchsocks[i].connected) | |
| 176 watchsocks[i].close(); | |
| 177 } | |
| 178 if (ss.playerconn.connected) { | |
| 179 ss.playerconn.sendUTF(JSON.stringify({"t": "q"})); | |
| 180 ss.playerconn.close(); | |
| 181 } | |
| 182 ss.emit('exit'); | |
| 183 gamemux.emit('end', ss.game.uname, ss.pname); | |
| 184 delete sessions[tag]; | |
| 185 tslog("Game %s ended.", tag); | |
| 186 }); | |
| 187 this.close = function () { | |
| 188 if (ss.tag() in sessions) | |
| 189 ss.term.kill('SIGHUP'); | |
| 190 }; | |
| 191 /* Send initial data. */ | |
| 192 this.playerconn.sendUTF(JSON.stringify({"t": "s", "w": this.w, "h": this.h, | |
| 193 "p": this.pname, "g": this.game.uname})); | |
| 194 /* Attach handlers. */ | |
| 195 function messageH(message) { | |
| 196 var parsedMsg = getMsgWS(message); | |
| 197 if (parsedMsg.t == 'q') { | |
| 198 ss.close(); | |
| 199 } | |
| 200 else if (parsedMsg.t == 'd') { | |
| 201 var hexstr = parsedMsg.d.replace(/[^0-9a-f]/gi, ""); | |
| 202 if (hexstr.length % 2 != 0) { | |
| 203 hexstr = hexstr.slice(0, -1); | |
| 204 } | |
| 205 var keybuf = new Buffer(hexstr, "hex"); | |
| 206 ss.write(keybuf); | |
| 207 } | |
| 208 } | |
| 209 this.playerconn.on('message', messageH); | |
| 210 this.playerconn.on('close', this.close); | |
| 211 /* To attach a watcher. */ | |
| 212 this.attach = function (wsReq) { | |
| 213 var conn = wsReq.accept(null, wsReq.origin); | |
| 214 conn.sendUTF(JSON.stringify({ | |
| 215 "t": "w", "w": this.w, "h": this.h, "p": this.pname, | |
| 216 "g": this.game.uname | |
| 217 })); | |
| 218 conn.sendUTF(JSON.stringify({"t": "d", | |
| 219 "d": this.framebuf.toString("hex", 0, this.frameoff)})); | |
| 220 conn.on('close', function () { | |
| 221 /* 'this' is the connection when triggered */ | |
| 222 var n = ss.watchers.indexOf(this); | |
| 223 if (n >= 0) { | |
| 224 ss.watchers.splice(n, 1); | |
| 225 tslog("A WebSocket watcher has left game %s", ss.tag()); | |
| 226 } | |
| 227 }); | |
| 228 this.watchers.push(conn); | |
| 229 }; | |
| 230 } | 149 } |
| 231 TermSession.prototype = new events.EventEmitter(); | 150 TermSession.prototype = new events.EventEmitter(); |
| 151 | |
| 232 TermSession.prototype.tag = function () { | 152 TermSession.prototype.tag = function () { |
| 233 if (this.pname === undefined || this.game === undefined) | 153 if (this.pname === undefined || this.gname === undefined) |
| 234 return ""; | 154 return ""; |
| 235 return this.game.uname + "/" + this.pname; | 155 return this.gname + "/" + this.pname; |
| 236 }; | 156 }; |
| 157 | |
| 237 TermSession.prototype.framepush = function(chunk) { | 158 TermSession.prototype.framepush = function(chunk) { |
| 238 /* If this chunk resets the screen, discard what preceded it. */ | 159 /* If this chunk resets the screen, discard what preceded it. */ |
| 239 if (isclear(chunk)) { | 160 if (isclear(chunk)) { |
| 240 this.framebuf = new Buffer(1024); | 161 this.framebuf = new Buffer(1024); |
| 241 this.frameoff = 0; | 162 this.frameoff = 0; |
| 250 this.framebuf.length); | 171 this.framebuf.length); |
| 251 } | 172 } |
| 252 } | 173 } |
| 253 chunk.copy(this.framebuf, this.frameoff); | 174 chunk.copy(this.framebuf, this.frameoff); |
| 254 this.frameoff += chunk.length; | 175 this.frameoff += chunk.length; |
| 176 }; | |
| 177 | |
| 178 /* Currently this also sends to the player and any watchers. */ | |
| 179 TermSession.prototype.write_ttyrec = function (datastr) { | |
| 180 this.lasttime = new Date(); | |
| 181 var buf = new Buffer(datastr); | |
| 182 var chunk = new Buffer(buf.length + 12); | |
| 183 /* TTYREC headers */ | |
| 184 chunk.writeUInt32LE(Math.floor(this.lasttime.getTime() / 1000), 0); | |
| 185 chunk.writeUInt32LE(1000 * (this.lasttime.getTime() % 1000), 4); | |
| 186 chunk.writeUInt32LE(buf.length, 8); | |
| 187 buf.copy(chunk, 12); | |
| 188 this.record.write(chunk); | |
| 189 this.framepush(buf); | |
| 190 /* Send to the player. */ | |
| 191 var msg = JSON.stringify({"t": "d", "d": buf.toString("hex")}); | |
| 192 this.playerconn.sendUTF(msg); | |
| 193 /* Send to any watchers. */ | |
| 194 for (var i = 0; i < this.watchers.length; i++) { | |
| 195 if (this.watchers[i].connected) | |
| 196 this.watchers[i].sendUTF(msg); | |
| 197 } | |
| 198 this.emit('data', buf); | |
| 199 }; | |
| 200 | |
| 201 /* For writing to the subprocess's stdin. */ | |
| 202 TermSession.prototype.write = function (data) { | |
| 203 this.term.write(data); | |
| 204 }; | |
| 205 | |
| 206 TermSession.prototype.input_msg = function (message) { | |
| 207 var parsedMsg = getMsgWS(message); | |
| 208 if (parsedMsg.t == 'q') { | |
| 209 this.close(); | |
| 210 } | |
| 211 else if (parsedMsg.t == 'd') { | |
| 212 var hexstr = parsedMsg.d.replace(/[^0-9a-f]/gi, ""); | |
| 213 if (hexstr.length % 2 != 0) { | |
| 214 hexstr = hexstr.slice(0, -1); | |
| 215 } | |
| 216 var keybuf = new Buffer(hexstr, "hex"); | |
| 217 this.write(keybuf); | |
| 218 } | |
| 219 }; | |
| 220 | |
| 221 /* Teardown. */ | |
| 222 TermSession.prototype.close = function () { | |
| 223 if (this.tag() in sessions) | |
| 224 this.term.kill('SIGHUP'); | |
| 225 }; | |
| 226 | |
| 227 TermSession.prototype.destroy = function () { | |
| 228 var tag = this.tag(); | |
| 229 fs.unlink(this.lock); | |
| 230 this.record.end(); | |
| 231 var watchsocks = this.watchers; | |
| 232 this.watchers = []; | |
| 233 for (var i = 0; i < watchsocks.length; i++) { | |
| 234 if (watchsocks[i].connected) | |
| 235 watchsocks[i].close(); | |
| 236 } | |
| 237 if (this.playerconn.connected) { | |
| 238 this.playerconn.sendUTF(JSON.stringify({"t": "q"})); | |
| 239 this.playerconn.close(); | |
| 240 } | |
| 241 this.emit('exit'); | |
| 242 gamemux.emit('end', this.gname, this.pname); | |
| 243 delete sessions[tag]; | |
| 244 tslog("Game %s ended.", tag); | |
| 245 }; | |
| 246 | |
| 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 } | |
| 255 }; | 265 }; |
| 256 | 266 |
| 257 function DglSession(filename) { | 267 function DglSession(filename) { |
| 258 var ss = this; | 268 var ss = this; |
| 259 events.EventEmitter.call(this); | 269 events.EventEmitter.call(this); |
