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