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); |