Mercurial > hg > rlgwebd
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 |