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 |
