comparison rlgwebd @ 195:3bdee6371c3f

Change various filenames. The shell script previously used to launch the daemon is now called "initscript". The script files have had the ".js" extension removed from their names.
author John "Elwin" Edwards
date Thu, 14 Jan 2016 20:52:29 -0500
parents rlgwebd.js@5483d413a45b
children ea28353d620a
comparison
equal deleted inserted replaced
194:5483d413a45b 195:3bdee6371c3f
1 #!/bin/sh 1 #!/usr/bin/env node
2 2
3 NODE_PATH=/usr/lib/node_modules 3 var http = require('http');
4 LOGFILE=/var/local/rlgwebd/log 4 var net = require('net');
5 CTLSOCKET=/var/run/rlgwebd.sock 5 var url = require('url');
6 RLGWEBDJS=./rlgwebd.js 6 var path = require('path');
7 7 var fs = require('fs');
8 export NODE_PATH 8 var events = require('events');
9 9 var child_process = require('child_process');
10 if [ $UID != 0 ] 10 // Dependencies
11 then 11 var posix = require("posix");
12 echo "$0 needs to run as root." >&2 12 var pty = require("pty.js");
13 exit 1 13 var WebSocketServer = require("websocket").server;
14 fi 14
15 15 /* Configuration variables */
16 if [ $# -gt 0 ] && [ $1 = stop ] 16 // The first file is NOT in the chroot.
17 then 17 var ctlsocket = "/var/run/rlgwebd.sock";
18 socat "EXEC:echo quit" "$CTLSOCKET" 18 var httpPort = 8080;
19 else 19 var chrootDir = "/var/dgl/";
20 # Start 20 var dropToUser = "rodney";
21 setsid node "$RLGWEBDJS" </dev/null &>>$LOGFILE & 21 var serveStaticRoot = "/var/www/"; // inside the chroot
22 fi 22
23 23 var clearbufs = [
24 exit 24 new Buffer([27, 91, 72, 27, 91, 50, 74]), // xterm: CSI H CSI 2J
25 25 new Buffer([27, 91, 72, 27, 91, 74]) // screen: CSI H CSI J
26 ];
27
28 /* Data on the games available. */
29 var games = {
30 "rogue3": {
31 "name": "Rogue V3",
32 "uname": "rogue3",
33 "suffix": ".r3sav",
34 "path": "/usr/bin/rogue3"
35 },
36 "rogue4": {
37 "name": "Rogue V4",
38 "uname": "rogue4",
39 "suffix": ".r4sav",
40 "path": "/usr/bin/rogue4"
41 },
42 "rogue5": {
43 "name": "Rogue V5",
44 "uname": "rogue5",
45 "suffix": ".r5sav",
46 "path": "/usr/bin/rogue5"
47 },
48 "srogue": {
49 "name": "Super-Rogue",
50 "uname": "srogue",
51 "suffix": ".srsav",
52 "path": "/usr/bin/srogue"
53 },
54 "arogue5": {
55 "name": "Advanced Rogue 5",
56 "uname": "arogue5",
57 "suffix": ".ar5sav",
58 "path": "/usr/bin/arogue5"
59 },
60 "arogue7": {
61 "name": "Advanced Rogue 7",
62 "uname": "arogue7",
63 "suffix": ".ar7sav",
64 "path": "/usr/bin/arogue7"
65 },
66 "xrogue": {
67 "name": "XRogue",
68 "uname": "xrogue",
69 "suffix": ".xrsav",
70 "path": "/usr/bin/xrogue"
71 }
72 };
73
74 /* Global state */
75 var logins = {};
76 var sessions = {};
77 var dglgames = {};
78 var allowlogin = true;
79 var gamemux = new events.EventEmitter();
80
81 /* A base class. TermSession and DglSession inherit from it. */
82 function BaseGame() {
83 /* Games subclass EventEmitter, though there are few listeners. */
84 events.EventEmitter.call(this);
85 /* Array of watching WebSockets. */
86 this.watchers = [];
87 /* replaybuf holds the output since the last screen clear, so watchers can
88 * begin with a complete screen. replaylen is the number of bytes stored. */
89 this.replaybuf = new Buffer(1024);
90 this.replaylen = 0;
91 /* Time of last activity. */
92 this.lasttime = new Date();
93 }
94 BaseGame.prototype = new events.EventEmitter();
95
96 BaseGame.prototype.tag = function () {
97 if (this.pname === undefined || this.gname === undefined)
98 return "";
99 return this.gname + "/" + this.pname;
100 };
101
102 BaseGame.prototype.framepush = function(chunk) {
103 /* If this chunk resets the screen, discard what preceded it. */
104 if (isclear(chunk)) {
105 this.replaybuf = new Buffer(1024);
106 this.replaylen = 0;
107 }
108 /* Make sure there's space. */
109 while (this.replaybuf.length < chunk.length + this.replaylen) {
110 var nbuf = new Buffer(this.replaybuf.length * 2);
111 this.replaybuf.copy(nbuf, 0, 0, this.replaylen);
112 this.replaybuf = nbuf;
113 if (this.replaybuf.length > 65536) {
114 tslog("Warning: %s frame buffer at %d bytes", this.tag(),
115 this.replaybuf.length);
116 }
117 }
118 chunk.copy(this.replaybuf, this.replaylen);
119 this.replaylen += chunk.length;
120 };
121
122 /* Adds a watcher. */
123 BaseGame.prototype.attach = function (wsReq) {
124 var conn = wsReq.accept(null, wsReq.origin);
125 conn.sendUTF(JSON.stringify({
126 "t": "w", "w": this.w, "h": this.h, "p": this.pname, "g": this.gname
127 }));
128 conn.sendUTF(JSON.stringify({"t": "d",
129 "d": this.replaybuf.toString("hex", 0, this.replaylen)}));
130 conn.on('close', this.detach.bind(this, conn));
131 this.watchers.push(conn);
132 };
133
134 BaseGame.prototype.detach = function (socket) {
135 var n = this.watchers.indexOf(socket);
136 if (n >= 0) {
137 this.watchers.splice(n, 1);
138 tslog("A WebSocket watcher has left game %s", this.tag());
139 }
140 };
141
142 /* Constructor. A TermSession handles a pty and the game running on it.
143 * gname: (String) Name of the game to launch.
144 * pname: (String) The player's name.
145 * wsReq: (WebSocketRequest) The request from the client.
146 *
147 * Events:
148 * "data": Data generated by child. Parameters: buf (Buffer)
149 * "exit": Child terminated. Parameters: none
150 */
151 function TermSession(gname, pname, wsReq) {
152 BaseGame.call(this);
153 /* Don't launch anything that's not a real game. */
154 if (gname in games) {
155 this.game = games[gname];
156 this.gname = gname;
157 }
158 else {
159 this.failed = true;
160 wsReq.reject(404, errorcodes[2], "No such game");
161 tslog("Game %s is not available", game);
162 return;
163 }
164 this.pname = pname;
165 /* Set up the sizes. */
166 this.w = Math.floor(Number(wsReq.resourceURL.query.w));
167 if (!(this.w > 0 && this.w < 256))
168 this.w = 80;
169 this.h = Math.floor(Number(wsReq.resourceURL.query.h));
170 if (!(this.h > 0 && this.h < 256))
171 this.h = 24;
172 /* Environment. */
173 var childenv = {};
174 for (var key in process.env) {
175 childenv[key] = process.env[key];
176 }
177 var args = ["-n", this.pname];
178 var spawnopts = {"env": childenv, "cwd": "/", "rows": this.h, "cols": this.w,
179 "name": "xterm-256color"};
180 this.term = pty.spawn(this.game.path, args, spawnopts);
181 tslog("%s playing %s (pid %d)", this.pname, this.gname, this.term.pid);
182 this.failed = false;
183 sessions[this.gname + "/" + this.pname] = this;
184 gamemux.emit('begin', this.gname, this.pname, 'rlg');
185 /* Set up the lockfile and ttyrec */
186 var ts = timestamp(this.lasttime);
187 var progressdir = path.join("/dgldir/inprogress", this.gname);
188 this.lock = path.join(progressdir, this.pname + ":node:" + ts + ".ttyrec");
189 var lmsg = this.term.pid.toString() + '\n' + this.h + '\n' + this.w + '\n';
190 fs.writeFile(this.lock, lmsg, "utf8");
191 var ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname,
192 ts + ".ttyrec");
193 this.record = fs.createWriteStream(ttyrec, { mode: 0664 });
194 /* The player's WebSocket and its handlers. */
195 this.playerconn = wsReq.accept(null, wsReq.origin);
196 this.playerconn.on('message', this.input_msg.bind(this));
197 this.playerconn.on('close', this.close.bind(this));
198 /* Send initial data. */
199 this.playerconn.sendUTF(JSON.stringify({"t": "s", "w": this.w, "h": this.h,
200 "p": this.pname, "g": this.gname}));
201 /* Begin the ttyrec with some metadata, like dgamelaunch does. */
202 var descstr = "\x1b[2J\x1b[1;1H\r\n";
203 descstr += "Player: " + this.pname + "\r\nGame: " + this.game.name + "\r\n";
204 descstr += "Server: Roguelike Gallery - rlgallery.org\r\n";
205 descstr += "Filename: " + ts + ".ttyrec\r\n";
206 descstr += "Time: (" + Math.floor(this.lasttime.getTime() / 1000) + ") ";
207 descstr += this.lasttime.toUTCString().slice(0, -4) + "\r\n";
208 descstr += "Size: " + this.w + "x" + this.h + "\r\n\x1b[2J";
209 this.write_ttyrec(descstr);
210 this.term.on("data", this.write_ttyrec.bind(this));
211 this.term.on("exit", this.destroy.bind(this));
212 }
213 TermSession.prototype = new BaseGame();
214
215 /* Currently this also sends to the player and any watchers. */
216 TermSession.prototype.write_ttyrec = function (datastr) {
217 this.lasttime = new Date();
218 var buf = new Buffer(datastr);
219 var chunk = new Buffer(buf.length + 12);
220 /* TTYREC headers */
221 chunk.writeUInt32LE(Math.floor(this.lasttime.getTime() / 1000), 0);
222 chunk.writeUInt32LE(1000 * (this.lasttime.getTime() % 1000), 4);
223 chunk.writeUInt32LE(buf.length, 8);
224 buf.copy(chunk, 12);
225 this.record.write(chunk);
226 this.framepush(buf);
227 /* Send to the player. */
228 var msg = JSON.stringify({"t": "d", "d": buf.toString("hex")});
229 this.playerconn.sendUTF(msg);
230 /* Send to any watchers. */
231 for (var i = 0; i < this.watchers.length; i++) {
232 if (this.watchers[i].connected)
233 this.watchers[i].sendUTF(msg);
234 }
235 this.emit('data', buf);
236 };
237
238 /* For writing to the subprocess's stdin. */
239 TermSession.prototype.write = function (data) {
240 this.term.write(data);
241 };
242
243 TermSession.prototype.input_msg = function (message) {
244 var parsedMsg = getMsgWS(message);
245 if (parsedMsg.t == 'q') {
246 this.close();
247 }
248 else if (parsedMsg.t == 'd') {
249 var hexstr = parsedMsg.d.replace(/[^0-9a-f]/gi, "");
250 if (hexstr.length % 2 != 0) {
251 hexstr = hexstr.slice(0, -1);
252 }
253 var keybuf = new Buffer(hexstr, "hex");
254 this.write(keybuf);
255 }
256 };
257
258 /* Teardown. */
259 TermSession.prototype.close = function () {
260 if (this.tag() in sessions)
261 this.term.kill('SIGHUP');
262 };
263
264 TermSession.prototype.destroy = function () {
265 var tag = this.tag();
266 fs.unlink(this.lock);
267 this.record.end();
268 var watchsocks = this.watchers;
269 this.watchers = [];
270 for (var i = 0; i < watchsocks.length; i++) {
271 if (watchsocks[i].connected)
272 watchsocks[i].close();
273 }
274 if (this.playerconn.connected) {
275 this.playerconn.sendUTF(JSON.stringify({"t": "q"}));
276 this.playerconn.close();
277 }
278 this.emit('exit');
279 gamemux.emit('end', this.gname, this.pname);
280 delete sessions[tag];
281 tslog("Game %s ended.", tag);
282 };
283
284 function DglSession(filename) {
285 BaseGame.call(this);
286 var pathcoms = filename.split('/');
287 this.gname = pathcoms[pathcoms.length - 2];
288 if (!(this.gname in games)) {
289 this.emit('open', false);
290 return;
291 }
292 var basename = pathcoms[pathcoms.length - 1];
293 var firstsep = basename.indexOf(':');
294 this.pname = basename.slice(0, firstsep);
295 var fname = basename.slice(firstsep + 1);
296 this.ttyrec = path.join("/dgldir/ttyrec", this.pname, this.gname, fname);
297 /* Flag to prevent multiple handlers from reading simultaneously and
298 * getting into a race. */
299 this.reading = false;
300 this.rpos = 0;
301 fs.readFile(filename, {encoding: "utf8"}, (function (err, data) {
302 if (err) {
303 this.emit('open', false);
304 return;
305 }
306 var lines = data.split('\n');
307 this.h = Number(lines[1]);
308 this.w = Number(lines[2]);
309 fs.open(this.ttyrec, "r", (function (err, fd) {
310 if (err) {
311 this.emit('open', false);
312 }
313 else {
314 this.fd = fd;
315 this.emit('open', true);
316 tslog("DGL %s: open", this.tag());
317 gamemux.emit('begin', this.gname, this.pname, 'dgl');
318 this.startchunk();
319 this.recwatcher = fs.watch(this.ttyrec, this.notifier.bind(this));
320 }
321 }).bind(this));
322 }).bind(this));
323 }
324 DglSession.prototype = new BaseGame();
325
326 /* 3 functions to get data from the ttyrec file. */
327 DglSession.prototype.startchunk = function () {
328 if (this.reading)
329 return;
330 this.reading = true;
331 var header = new Buffer(12);
332 fs.read(this.fd, header, 0, 12, this.rpos, this.datachunk.bind(this));
333 };
334
335 DglSession.prototype.datachunk = function (err, n, buf) {
336 /* Stop recursion if end of file has been reached. */
337 if (err || n < 12) {
338 if (!err && n > 0) {
339 tslog("DGL %s: expected 12-byte header, got %d", this.tag(), n);
340 }
341 this.reading = false;
342 return;
343 }
344 this.rpos += 12;
345 /* Update timestamp, to within 1 second. */
346 this.lasttime = new Date(1000 * buf.readUInt32LE(0));
347 var datalen = buf.readUInt32LE(8);
348