├── .gitignore ├── bin ├── psm-worker ├── psm └── psmd ├── TODO.md ├── config └── database.yml.example ├── package.json ├── lib ├── psm │ ├── worker │ │ ├── helpers │ │ │ ├── jobs.js │ │ │ ├── system.js │ │ │ ├── players.js │ │ │ ├── logs.js │ │ │ ├── plugins.js │ │ │ ├── proc.js │ │ │ ├── files.js │ │ │ ├── output.js │ │ │ └── worlds.js │ │ └── worker.js │ ├── models.js │ ├── manager │ │ ├── api.js │ │ └── manager.js │ └── cli.js └── psm.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | logs/* 3 | config/* 4 | config.json 5 | *~ 6 | \#*# -------------------------------------------------------------------------------- /bin/psm-worker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var Worker = require('../lib/psm/worker/worker').Worker, 4 | worker = new Worker(); -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | 3 | - Seperate file logs (see config) 4 | - Adding server via cli prompts 5 | - Make CLI use hook communication instead of REST? -------------------------------------------------------------------------------- /config/database.yml.example: -------------------------------------------------------------------------------- 1 | mongodb: 2 | #this is mongodb://:@:/", 3 | "name": "psm", 4 | "description": "Panther Server Manager manages servers and provides a common CLI and API", 5 | "version": "0.2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/pantherdev/psm.git" 9 | }, 10 | "bin": { 11 | "psm": "./bin/psm", 12 | "psmd": "./bin/psmd" 13 | }, 14 | "engines": { 15 | "node": "v0.6.x" 16 | }, 17 | "dependencies": { 18 | "mongoose-auth": "0.0.x", 19 | 20 | "colors": "0.6.x", 21 | "eyes": "0.1.x", 22 | "express": "2.x.x", 23 | "utile": "0.1.x", 24 | "pkginfo": "0.2.x", 25 | "flatiron": "0.1.x", 26 | "mongoose": "2.6.x", 27 | "request": "2.9.x", 28 | "cron": "0.2.x", 29 | "js-yaml": "0.3.x", 30 | "socket.io": "0.9.x", 31 | "hook.io": "0.8.x", 32 | "lumber": "git+https://github.com/englercj/lumber.git" 33 | }, 34 | "devDependencies": {}, 35 | "private": true 36 | } -------------------------------------------------------------------------------- /bin/psmd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var psm = require('../lib/psm'), 4 | fs = require('fs'), 5 | Manager = require('../lib/psm/manager/manager').Manager, 6 | man = new Manager({ name: 'manager' }); 7 | 8 | psm.init(function(err) { 9 | if(err) { process.ext(1); return; } 10 | 11 | man.start(); 12 | 13 | man.once('hook::ready', function() { 14 | //pid management 15 | var pid; 16 | try { 17 | pid = fs.readFileSync(psm.config.pids.manager); 18 | } catch(e) {} 19 | 20 | if(pid) { 21 | psm.log.error('PSM Daemon is already running!'); 22 | process.exit(2); 23 | } else { 24 | man.startup(function(err) { 25 | if(err) psm.log.error(err, 'Error starting manager'); 26 | }); 27 | 28 | fs.writeFileSync(psm.config.pids.manager, process.pid); 29 | 30 | process.on('uncaughtException', function(err) { 31 | psm.log.error(err, 'Uncaught Exception in PSM Daemon'); 32 | process.exit(1); 33 | }); 34 | 35 | process.on('exit', function() { 36 | try { fs.unlinkSync(psm.config.pids.manager); } 37 | catch(e) {} 38 | }); 39 | } 40 | }); 41 | }); -------------------------------------------------------------------------------- /lib/psm/worker/helpers/jobs.js: -------------------------------------------------------------------------------- 1 | var utile = require('utile'), 2 | events = require('events'), 3 | cp = require('child_process'), 4 | path = require('path'), 5 | request = require('request'); 6 | 7 | var JobManager = exports.JobManager = function(options) { 8 | var self = this; 9 | events.EventEmitter.call(self); 10 | options = options || {}; 11 | 12 | self.log = options.logger; 13 | self.worker = options.worker; 14 | self.server = options.server; 15 | 16 | self._defineGetters(); 17 | self._defineSetters(); 18 | }; 19 | 20 | utile.inherits(JobManager, events.EventEmitter); 21 | 22 | JobManager.prototype.add = function(job, cb) { 23 | var self = this; 24 | }; 25 | 26 | JobManager.prototype.remove = function(job, cb) { 27 | var self = this; 28 | }; 29 | 30 | JobManager.prototype.get = function(cb) { 31 | var self = this; 32 | }; 33 | 34 | JobManager.prototype.getRunning = function(cb) { 35 | var self = this; 36 | }; 37 | 38 | JobManager.prototype.run = function(job, cb) { 39 | var self = this; 40 | }; 41 | 42 | JobManager.prototype._defineGetters = function(cb) { 43 | var self = this; 44 | 45 | //self.__defineGetter__('something', function() {}); 46 | }; 47 | 48 | JobManager.prototype._defineSetters = function(cb) { 49 | var self = this; 50 | 51 | //self.__defineSetter__('something', function(val) {}); 52 | }; 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Panther Server Manager (v0.0.1) 2 | 3 | ## Overview 4 | 5 | Panther Server Manager is a [Node.js](http://nodejs.org) application designed to manage [Minecraft](http://minecraft.com) game servers. This 6 | module includes the PSM Daemon (psmd) that can manage multiple minecraft server instances. The PSM service also provides a RESTful API 7 | for controlling all the minecraft servers. 8 | 9 | This module also provides a CLI application (psm) that wraps around the API for easy use from the command line. 10 | 11 | ## Features 12 | 13 | - Centralized management for all your Minecraft servers 14 | - CLI Interface 15 | - RESTful API 16 | 17 | ## Dependencies 18 | 19 | - Node.js (0.6.x) 20 | - Npm (1.x.x) 21 | - MongoDB 22 | 23 | ## Installation 24 | 25 | The easiest way to install the module is with `npm` (Coming Soon): 26 | 27 | ```bash 28 | npm install psm -g 29 | ``` 30 | 31 | You can manually clone the repo and install with this script (still requires npm): 32 | 33 | ```bash 34 | git clone git://github.com/pantherdev/psm.git && 35 | cd psm && 36 | npm install -g 37 | ``` 38 | 39 | ## Configuration 40 | 41 | The `database.yml` file in the `config/` directory holds the connection information for you MongoDB connection. 42 | The application will store its managed servers, remotes, and other settings in MongoDB. Please edit 43 | the `database.yml` file before attempting to start the psmd service. 44 | 45 | ### Example `database.yml` 46 | 47 | ```yaml 48 | mongodb: 49 | #this is mongodb://:@:/ -1) 55 | cb(null, self._players[i]); 56 | else 57 | cb(new Error('Player not connected.')); 58 | }; 59 | 60 | PlayerManager.prototype.getInventory = function(player, cb) { 61 | var self = this; 62 | }; 63 | 64 | PlayerManager.prototype.clearInventorySlot = function(player, slot, cb) { 65 | var self = this; 66 | }; 67 | 68 | PlayerManager.prototype.kill = function(player, cb) { 69 | var self = this; 70 | }; 71 | 72 | PlayerManager.prototype.setHealth = function(player, cb) { 73 | var self = this; 74 | }; 75 | 76 | PlayerManager.prototype.setFood = function(player, food, cb) { 77 | var self = this; 78 | }; 79 | 80 | PlayerManager.prototype.kick = function(player, cb) { 81 | var self = this; 82 | }; 83 | 84 | PlayerManager.prototype.ban = function(player, cb) { 85 | var self = this; 86 | }; 87 | 88 | PlayerManager.prototype.unban = function(player, cb) { 89 | var self = this; 90 | }; 91 | 92 | PlayerManager.prototype.addToWhitelist = function(player, cb) { 93 | var self = this; 94 | }; 95 | 96 | PlayerManager.prototype.removeFromWhitelist = function(player, cb) { 97 | var self = this; 98 | }; 99 | 100 | PlayerManager.prototype.op = function(player, cb) { 101 | var self = this; 102 | }; 103 | 104 | PlayerManager.prototype.deop = function(player, cb) { 105 | var self = this; 106 | }; 107 | 108 | PlayerManager.prototype._removePlayer = function(name) { 109 | var self = this, i = self._findPlayerIndex(name); 110 | 111 | if(i > -1) { 112 | self._players.splice(i, 1); 113 | return true; 114 | } else { 115 | return false; 116 | } 117 | }; 118 | 119 | PlayerManager.prototype._findPlayerIndex = function(name) { 120 | var self = this, indx = -1; 121 | 122 | for(var i = 0, len = self._players.length; i < len; ++i) { 123 | if(self._players[i].name == name) { 124 | indx = i; 125 | break; 126 | } 127 | } 128 | 129 | return indx; 130 | }; 131 | 132 | PluginManager.prototype._defineGetters = function() { 133 | var self = this; 134 | 135 | //self.__defineGetter__('something', function() {}); 136 | }; 137 | 138 | PluginManager.prototype._defineSetters = function() { 139 | var self = this; 140 | 141 | //self.__defineSetter__('something', function(val) {}); 142 | }; 143 | -------------------------------------------------------------------------------- /lib/psm/worker/helpers/logs.js: -------------------------------------------------------------------------------- 1 | var utile = require('utile'), 2 | events = require('events'), 3 | async = utile.async, 4 | fs = require('fs'), 5 | path = require('path'); 6 | 7 | var LogManager = exports.LogManager = function(options) { 8 | var self = this; 9 | events.EventEmitter.call(self); 10 | options = options || {}; 11 | 12 | self.log = options.logger; 13 | self.worker = options.worker; 14 | self.server = options.server; 15 | 16 | self.paths = options.server.paths; 17 | 18 | self._defineGetters(); 19 | self._defineSetters(); 20 | 21 | self._bufferMax = 200; 22 | self._outputBuffer = []; 23 | self._chatBuffer = []; 24 | 25 | self.worker.outputs.on('output', function(str) { 26 | self._outputBuffer = self._outputBuffer.concat(str); 27 | 28 | if(self._outputBuffer.length > self._bufferMax) 29 | self._outputBuffer.unshift(); 30 | }); 31 | 32 | self.worker.outputs.on('player::chat', function(time, player, msg) { 33 | self._chatBuffer.push({ 34 | timestamp: time, 35 | player: player, 36 | message: msg 37 | }); 38 | 39 | if(self._chatBuffer.length > self._bufferMax) 40 | self._chatBuffer.unshift(); 41 | }); 42 | }; 43 | 44 | utile.inherits(LogManager, events.EventEmitter); 45 | 46 | LogManager.prototype.backup = function(cb) { 47 | /* 48 | var self = this; 49 | 50 | if(self.worker.isRunning()) { 51 | self.worker.stop(function(err) { 52 | if(err) { 53 | self.log.error('Unable to stop server for log backups', err); 54 | if(cb) cb(err); 55 | return; 56 | } 57 | 58 | self._doLogBackup(cb); 59 | }); 60 | } else { 61 | self._doLogBackup(cb); 62 | } 63 | */ 64 | 65 | var self = this, 66 | logP = self.paths.log, 67 | bak = self.backup.logs.path, 68 | fname = self.worker.files._datePath(path.join(bak, 'serverlog_'), '.log'); 69 | 70 | self.log.info('Backing up server.log to ' + fname + '...'); 71 | 72 | utile.mkdirp(bak, function(err) { 73 | if(err) { if(cb) cb(err); return; } 74 | 75 | utile.cpr(logP, fname, cb/*function(err) { 76 | if(err) { 77 | if(err.code == 'ENOENT') { 78 | self.log.info('No server.log to backup.'); 79 | } 80 | 81 | //self.worker.start(function() { 82 | if(cb) cb(err); 83 | //}); 84 | return; 85 | } 86 | 87 | self.log.info('Gzipping backed up logfile...'); 88 | cp.exec('gzip ' + fname, function(err, stdout, stderr) { 89 | if(err) { 90 | self.log.error('Error gzippping backup logfile ' + fname, err); 91 | } 92 | 93 | //self.worker.start(function() { 94 | if(cb) cb(err); 95 | //}); 96 | }); 97 | }*/); 98 | }); 99 | }; 100 | 101 | LogManager.prototype.restore = function(file, cb) { 102 | var self = this; 103 | }; 104 | 105 | LogManager.prototype.get = function(cb) { 106 | var self = this; 107 | 108 | self.worker.files.read(self.paths.log, cb); 109 | }; 110 | 111 | //Pull tails out of an in-memory buffer of previous lines 112 | LogManager.prototype.tail = function(limit, cb) { 113 | var self = this, 114 | lines = self._outputBuffer; 115 | 116 | if(typeof(limit) == 'function') { 117 | cb = limit; 118 | limit = 0; 119 | } 120 | 121 | cb(null, (limit ? lines.slice(line.length - limit) : lines)); 122 | }; 123 | 124 | LogManager.prototype.tailChats = function(limit, player, cb) { 125 | var self = this, 126 | chats = self._chatBuffer; 127 | 128 | if(typeof(player) == 'function') { 129 | cb = player; 130 | player = null; 131 | } 132 | 133 | if(typeof(limit) == 'function') { 134 | cb = limit; 135 | limit = 0; 136 | } 137 | 138 | //filter based on player if passed 139 | if(player) { 140 | chats = chats.filter(function(chat) { 141 | return chat.player == player; 142 | }); 143 | } 144 | 145 | //limit if we have one 146 | cb(null, (limit ? chats.slice(chats.length - limit) : chats)); 147 | }; -------------------------------------------------------------------------------- /lib/psm/worker/helpers/plugins.js: -------------------------------------------------------------------------------- 1 | var utile = require('utile'), 2 | events = require('events'), 3 | path = require('path'), 4 | request = require('request'); 5 | 6 | var PluginManager = exports.PluginManager = function(options) { 7 | var self = this; 8 | events.EventEmitter.call(self); 9 | options = options || {}; 10 | 11 | self.log = options.logger; 12 | self.worker = options.worker; 13 | self.server = options.server; 14 | 15 | self._defineGetters(); 16 | self._defineSetters(); 17 | }; 18 | 19 | utile.inherits(PluginManager, events.EventEmitter); 20 | 21 | PluginManager.prototype.get = function(cb) { 22 | var self = this; 23 | }; 24 | 25 | PluginManager.prototype.getRunning = function(cb) { 26 | //TODO: Buffer this on plugin startup, and on reload 27 | //I don't think it can change in between... 28 | 29 | //use command "plugins" to get plugin list 30 | var self = this, 31 | timeout = setTimeout(pluginsGetTimeout, 2500), 32 | parser = self.worker.outputs._addParser(function(str) { 33 | var parts = str.match(/^$/); 34 | 35 | if(parts) { 36 | //0 = entire message 37 | //1 = timestamp 38 | //2 = ... 39 | clearTimeout(timeout); 40 | self.worker.outputs._removeParser(parser); 41 | if(cb) cb(null, { 42 | timestamp: parts[1], 43 | plugins: parts.slice(2) 44 | }); 45 | } 46 | }); 47 | 48 | //fire off the 'plugins' command 49 | self.worker.cmd('plugins'); 50 | 51 | function pluginsGetTimeout() { 52 | self.worker.outputs._removeParser(parser); 53 | if(cb) cb(new Error('Command timed out.')); 54 | } 55 | }; 56 | 57 | PluginManager.prototype.install = function(jar, cb) { 58 | var self = this; 59 | }; 60 | 61 | PluginManager.prototype.installFromUrl = function(url, cb) { 62 | var self = this; 63 | }; 64 | 65 | PluginManager.prototype.remove = function(plugin, rmDir, cb) { 66 | var self = this; 67 | 68 | if(typeof(rmDir) == 'function') { 69 | cb = rmDir; 70 | rmDir = false; 71 | } 72 | }; 73 | 74 | PluginManager.prototype.disable = function(plugin, cb) { 75 | var self = this; 76 | }; 77 | 78 | PluginManager.prototype.enable = function(plugin, cb) { 79 | var self = this; 80 | }; 81 | 82 | PluginManager.prototype.info = function(plugin, cb) { 83 | var self = this; 84 | //http://bukget.org/api/plugin/ 85 | }; 86 | 87 | PluginManager.prototype.getDisabled = function(cb) { 88 | var self = this; 89 | }; 90 | 91 | PluginManager.prototype.reload = function(cb) { 92 | var self = this; 93 | 94 | //reloads plugins via the 'reload' command 95 | }; 96 | 97 | PluginManager.prototype.update = function(plugin, force, cb) { 98 | var self = this; 99 | 100 | if(typeof(force) == 'function') { 101 | cb = force; 102 | force = false; 103 | } 104 | 105 | self.checkUpdates(plugin, function(err, result) { 106 | if(result.upToDate && !force) { 107 | if(cb) cb(null, result); 108 | return; 109 | } 110 | 111 | self.remove(plugin, function(err) { 112 | if(err) { if(cb) cb(err); return; } 113 | 114 | self.installFromUrl(result.newVersion.dl_link, function(err) { 115 | if(err) { if(cb) cb(err); return; } 116 | 117 | if(cb) cb(); 118 | }); 119 | }); 120 | }); 121 | }; 122 | 123 | PluginManager.prototype.checkUpdates = function(plugin, cb) { 124 | var self = this; 125 | 126 | try { 127 | self._findPlugin(plugin, function(err, data) { 128 | if(err) { if(cb) cb(err); return; } 129 | 130 | //if no data, then not on bukget 131 | if(!data) { 132 | if(cb) cb(new Error('Plugin is not on Bukkit Dev')); 133 | return; 134 | } 135 | 136 | //check if md5 matches latest version 137 | var md5 = self.worker.files.md5File(/* file path */).toLowerCase(), 138 | latestMd5 = data.versions[0].md5.toLowerCase(), 139 | ver = self._findVersionByMd5(md5, data.versions), 140 | obj = { 141 | upToDate: md5 == latestMd5, 142 | oldVersion: ver, 143 | newVersion: data.versions[0] 144 | }; 145 | 146 | if(cb) cb(null, obj); 147 | }); 148 | } catch(e) { 149 | if(cb) cb(e); 150 | } 151 | }; 152 | 153 | PluginManager.prototype._findVersionByMd5 = function(md5, versions) { 154 | var match; 155 | 156 | for(var i = 0, len = versions.length; i < len; ++i) { 157 | if(md5 == versions[i].md5) { 158 | match = versions[i]; 159 | break; 160 | } 161 | } 162 | 163 | return match; 164 | }; 165 | 166 | PluginManager.prototype._findPlugin = function(plugin, cb) { 167 | try { 168 | request('http://bukget.org/api/plugin/' + plugin, function(err, res, body) { 169 | if(err) { if(cb) cb(err); return; } 170 | 171 | if(res.statusCode == 200 && body) { 172 | if(cb) cb(null, JSON.parse(body)); 173 | } 174 | 175 | if(cb) cb(); 176 | }); 177 | } catch(e) { 178 | if(cb) cb(e); 179 | } 180 | }; 181 | 182 | PluginManager.prototype._defineGetters = function() { 183 | var self = this; 184 | 185 | //self.__defineGetter__('something', function() {}); 186 | }; 187 | 188 | PluginManager.prototype._defineSetters = function() { 189 | var self = this; 190 | 191 | //self.__defineSetter__('something', function(val) {}); 192 | }; 193 | -------------------------------------------------------------------------------- /lib/psm/worker/helpers/proc.js: -------------------------------------------------------------------------------- 1 | var cp = require('child_process'), 2 | util = require('util'), 3 | path = require('path'), 4 | events = require('events'); 5 | 6 | var ProcManager = exports.ProcManager = function(options) { 7 | var self = this; 8 | events.EventEmitter.call(self); 9 | options = options || {}; 10 | 11 | self.log = options.logger; 12 | self.worker = options.worker; 13 | self.server = options.server; 14 | 15 | self._server = null; 16 | self._starting = false; 17 | self._stopping = false; 18 | self._idle = null; 19 | self._isIdle = false; 20 | 21 | self._cmd = 'java'; 22 | self._args = [ 23 | '-Xmx' + options.server.startup.maxMem, 24 | '-Xms' + options.server.startup.initMem, 25 | '-XX:ParallelGCThreads=' + options.server.startup.cpus 26 | ]; 27 | if(options.server.startup.extraArgs) 28 | self._args = self._args.concat(options.server.startup.extraArgs); 29 | 30 | self._args = self._args.concat([ 31 | '-jar', 32 | path.join(options.server.paths.bin, options.server.startup.jar), 33 | 'nogui' 34 | ]); 35 | self._cwd = options.server.paths.bin; 36 | 37 | self._defineGetters(); 38 | self._defineSetters(); 39 | }; 40 | 41 | util.inherits(ProcManager, events.EventEmitter); 42 | 43 | ProcManager.prototype.start = function(cb) { 44 | var self = this; 45 | 46 | function startupDone() { 47 | self.worker.outputs.removeListener('startup::done', startupDone); 48 | self.worker.outputs.removeListener('startup::fail', startupFail); 49 | self.removeListener('startup::fail', startupFail); 50 | self.removeListener('shutdown::done', startupFail); 51 | 52 | self._starting = false; 53 | if(cb) cb(null, self._server); 54 | } 55 | 56 | function startupFail(err) { 57 | self.worker.outputs.removeListener('startup::done', startupDone); 58 | self.worker.outputs.removeListener('startup::fail', startupFail); 59 | self.removeListener('startup::fail', startupFail); 60 | self.removeListener('shutdown::done', startupFail); 61 | 62 | self._starting = false; 63 | 64 | if(!(err instanceof Error)) 65 | err = new Error('Process exited with code ' + err + ' while starting'); 66 | 67 | if(cb) cb(err); 68 | } 69 | 70 | if(self.running) { 71 | self.log.debug('Attempted to start while already running'); 72 | if(cb) cb(new Error('Server is already running')); 73 | } else { 74 | self.log.info('Server is starting up...'); 75 | 76 | //spawn service 77 | self.log.debug(self._args, 'spawning server with cmd: %s, cwd: %s', self._cmd, self._cwd); 78 | self._server = cp.spawn(self._cmd, self._args, { cwd: self._cwd }); 79 | 80 | self.worker.outputs.stream = self._server.stderr; 81 | 82 | //start idle check 83 | self.resetIdle(); 84 | self._starting = true; 85 | 86 | //emit startup event 87 | self.emit('startup::start'); 88 | 89 | //register events 90 | self._server.on('exit', function(code) { 91 | self.log.silly('Server process exited with code: %d', code); 92 | self.emit('shutdown::done', code); 93 | }); 94 | 95 | self.worker.outputs.on('startup::done', startupDone); 96 | self.worker.outputs.on('startup::fail', startupFail); 97 | self.on('startup::fail', startupFail); 98 | self.on('shutdown::done', startupFail); 99 | } 100 | }; 101 | 102 | ProcManager.prototype.stop = function(cb) { 103 | var self = this; 104 | 105 | function shutdownDone() { 106 | self.removeListener('shutdown::done', shutdownDone); 107 | self.removeListener('shutdown::fail', shutdownFail); 108 | 109 | self._stopping = false; 110 | self._server = null; 111 | if(cb) cb(null); 112 | } 113 | 114 | function shutdownFail(err) { 115 | self._stopping = false; 116 | 117 | psm.log.warn('Server went idle while shutting down, killing the process'); 118 | self._server.kill(); 119 | } 120 | 121 | if(!self.running) { 122 | self.log.debug('Attempted to stop while not running'); 123 | if(cb) cb(new Error('Server is not running')); 124 | } else { 125 | self.log.info('Stopping server...'); 126 | 127 | //send stop command to server 128 | self.input('stop\n'); 129 | 130 | //reset idle check 131 | self.resetIdle(); 132 | self._stopping = true; 133 | 134 | //setup events 135 | self.on('shutdown::done', shutdownDone); 136 | self.on('shutdown::fail', shutdownFail); 137 | } 138 | }; 139 | 140 | ProcManager.prototype.input = function(str, cb) { 141 | var self = this; 142 | 143 | if(!self.running) if(cb) cb(new Error('Server is not running')); 144 | 145 | self._server.stdin.write(str); 146 | if(cb) cb(null); 147 | }; 148 | 149 | ProcManager.prototype.resetIdle = function() { 150 | var self = this; 151 | 152 | if(self._idleTime) { 153 | clearTimeout(self._idle); 154 | self._isIdle = false; 155 | self._idle = setTimeout(function() { 156 | self._isIdle = true; 157 | self.emit('idle'); 158 | 159 | //going idle while starting up or shutting down is a fail 160 | if(self._starting) self.emit('startup::fail', new Error('Server went idle while starting')); 161 | if(self._stopping) self.emit('shutdown::fail', new Error('Server went idle while stopping')); 162 | }, self._settings.idleTime); 163 | } 164 | }; 165 | 166 | ProcManager.prototype._defineGetters = function() { 167 | var self = this; 168 | 169 | self.__defineGetter__('running', function() { 170 | return !!self._server; 171 | }); 172 | 173 | self.__defineGetter__('starting', function() { 174 | return self._starting; 175 | }); 176 | 177 | self.__defineGetter__('stopping', function() { 178 | return self._stopping; 179 | }); 180 | 181 | self.__defineGetter__('idle', function() { 182 | return self._isIdle; 183 | }); 184 | }; 185 | 186 | ProcManager.prototype._defineSetters = function() { 187 | 188 | }; -------------------------------------------------------------------------------- /lib/psm/worker/helpers/files.js: -------------------------------------------------------------------------------- 1 | var utile = require('utile'), 2 | events = require('events'), 3 | cp = require('child_process'), 4 | async = utile.async, 5 | fs = require('fs'), 6 | path = require('path'), 7 | conJob = require('cron').CronJob, 8 | request = require('request'); 9 | 10 | var FileManager = exports.FileManager = function(options) { 11 | var self = this; 12 | events.EventEmitter.call(self); 13 | options = options || {}; 14 | 15 | self.log = options.logger; 16 | self.worker = options.worker; 17 | self.server = options.server; 18 | 19 | self.paths = options.server.paths; 20 | self.backup = options.server.backup; 21 | self.worlds = options.server.worlds; 22 | self.ramWorlds = options.server.ramWorlds; 23 | 24 | self._defineGetters(); 25 | self._defineSetters(); 26 | 27 | self._setupBackupCrons(); 28 | }; 29 | 30 | utile.inherits(FileManager, events.EventEmitter); 31 | 32 | FileManager.prototype.read = function(file, encoding, cb) { 33 | var self = this; 34 | 35 | if(typeof(encoding) == 'function') { 36 | cb = encoding; 37 | encoding = 'utf8'; 38 | } 39 | 40 | encoding = encoding || 'utf8'; 41 | 42 | fs.readFile(file, encoding, cb); 43 | }; 44 | 45 | FileManager.prototype.rm = function(file, cb) { 46 | var self = this; 47 | 48 | fs.unlink(file, cb); 49 | }; 50 | 51 | FileManager.prototype.rmdir = function(dir, cb) { 52 | var self = this; 53 | 54 | utile.rimraf(dir, cb); 55 | }; 56 | 57 | FileManager.prototype.write = function(file, data, type, cb) { 58 | var self = this, 59 | encoding = 'utf8'; 60 | 61 | if(typeof(type) == 'function') { 62 | cb = type; 63 | type = 'json'; 64 | } 65 | 66 | type = type || 'json'; 67 | 68 | switch(type) { 69 | case 'yaml': //TODO, yaml encoding 70 | case 'json': data = JSON.stringify(data); break; 71 | case 'text': break; //don't encode data, and use default utf8 72 | case 'binary': ecoding = 'binary'; break; 73 | } 74 | 75 | fs.writeFile(file, data, encoding, cb); 76 | }; 77 | 78 | FileManager.prototype.list = function(dir, cb) { 79 | var self = this; 80 | 81 | fs.readdir(dir, cb); 82 | }; 83 | 84 | FileManager.prototype.listFiles = function(dir, match, cb) { 85 | var self = this, 86 | list; 87 | 88 | if(typeof(match) == 'function') { 89 | cb = match; 90 | match = null; 91 | } 92 | 93 | self.list(dir, function(err, files) { 94 | if(err) { if(cb) cb(err); return; } 95 | 96 | list = files.filter(function(file, i) { 97 | var f = fs.statSync(path.join(dir, file)), 98 | pass = (f.isFile() && (match ? match.test(file) : true)); 99 | 100 | return pass; 101 | }); 102 | 103 | if(cb) cb(null, list); 104 | }); 105 | }; 106 | 107 | FileManager.prototype.listDirs = function(dir, match, cb) { 108 | var self = this, 109 | list; 110 | 111 | if(typeof(match) == 'function') { 112 | cb = match; 113 | match = null; 114 | } 115 | 116 | self.list(dir, function(err, files) { 117 | if(err) { if(cb) cb(err); return; } 118 | 119 | list = files.filter(function(file, i) { 120 | var f = fs.statSync(path.join(dir, file)), 121 | pass = (f.isDirectory() && (match ? match.test(file) : true)); 122 | 123 | return pass; 124 | }); 125 | 126 | if(cb) cb(null, list); 127 | }); 128 | }; 129 | 130 | FileManager.prototype._datePath = function(p, ext, forceTime) { 131 | var self = this, 132 | d = (new Date()), 133 | date = d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate(), 134 | time = d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds(), 135 | path = p + date + ext, 136 | test; 137 | 138 | ext = ext || ''; 139 | 140 | try { 141 | test = fs.statSync(path); 142 | } catch(e) {} 143 | 144 | if((test || forceTime) && forceTime !== false) 145 | path = p + date + 'T' + time + ext; 146 | 147 | return path; 148 | }; 149 | 150 | FileManager.prototype._updateFile = function(file, uri, cb) { 151 | var self = this, 152 | bin = self.paths.bin, 153 | fileNew = file + '.new', 154 | //mult = multimeter(process), 155 | size = 0, 156 | seen = 0, 157 | bar = null; 158 | 159 | if(self.isRunning()) { 160 | if(cb) cb(new Error('Cannot update ' + file + ' while the server is running.')); 161 | return; 162 | } 163 | 164 | self.log.debug('Downloading updated ' + file + '...'); 165 | var req = request(uri, function() { 166 | var f; 167 | try { f = fs.lstatSync(path.join(bin, fileNew)); } 168 | catch(e) {} 169 | 170 | if(f) { 171 | cp.exec('diff ' + path.join(bin, file) + ' ' + path.join(bin, fileNew), function(err, stdout, stderr) { 172 | if(stdout.length === 0) { //the same 173 | fs.unlink(path.join(bin, fileNew), function(err) { 174 | if(cb) cb(err, false); 175 | }); 176 | } else { //dled new version 177 | fs.unlink(path.join(bin, file), function(err) { 178 | if(err) { if(cb) cb(err); return; } 179 | 180 | fs.rename(path.join(bin, fileNew), path.join(bin, file), function(err) { 181 | if(cb) cb(err, true); 182 | }); 183 | }); 184 | } 185 | }); 186 | } else { 187 | if(cb) cb(new Error('The file failed to download, unable to find new file ' + fileNew)); 188 | } 189 | }); 190 | 191 | req.on('response', function(res) { 192 | if(res.headers['content-length']) { 193 | size = res.headers['content-length']; 194 | } 195 | }); 196 | 197 | res.on('data', function(chunk) { 198 | seen += chunk.length; 199 | self.emit('update::progress::' + file, seen/size * 100); 200 | //if(bar) bar.percent(seen / size * 100); 201 | }); 202 | 203 | res.pipe(fs.createWriteStream(path.join(bin, fileNew))); 204 | 205 | /* 206 | mult.drop({ 207 | solid: { 208 | background: null, 209 | foreground: 'green', 210 | text: '|' 211 | } 212 | }, function(b) { bar = b; }); 213 | */ 214 | }; 215 | 216 | FileManager.prototype._defineGetters = function() { 217 | var self = this; 218 | 219 | self.__defineGetter__('properties', function() { 220 | if(!self._props) { 221 | try { 222 | self._props = {}; 223 | var lines = fs.readFileSync(path.join(self.paths.bin, 'server.properties'), 'utf8').split('\n'); 224 | 225 | lines.forEach(function(line) { 226 | if(line.charAt(0) == '#') return; 227 | 228 | var val = line.split('='); 229 | self._props[val[0]] = val[1]; 230 | }); 231 | } catch(e) { self._props = null; } 232 | } 233 | 234 | return self._props; 235 | }); 236 | }; 237 | 238 | FileManager.prototype._defineSetters = function() { 239 | 240 | }; -------------------------------------------------------------------------------- /lib/psm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * psm.js: Include for the Panther Server Manger module 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENCE 6 | * 7 | **/ 8 | 9 | ////////// 10 | // Required Includes 11 | /////////////////////////// 12 | var fs = require('fs'), 13 | path = require('path'), 14 | lumber = require('lumber'), 15 | //winston = require('winston'), 16 | utile = require('utile'), 17 | pkginfo = require('pkginfo'), 18 | request = require('request'), 19 | mongoose = require('mongoose'), 20 | mongooseAuth = require('mongoose-auth'), 21 | yaml = require('js-yaml'), 22 | schemas = require('./psm/models'), 23 | Manager = require('./psm/manager').Manager, 24 | Config = mongoose.model('Config'); 25 | 26 | ////////// 27 | // Setup exports 28 | /////////////////////////// 29 | var psm = exports; 30 | 31 | ///////// 32 | // Load db configuration 33 | /////////////////////////// 34 | psm.database = require(path.join(__dirname, '../config/database.yml')).shift().mongodb; 35 | 36 | ///////// 37 | // Helper Utils 38 | /////////////////////////// 39 | psm.generateGuid = function() { 40 | var S4 = function () { 41 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); 42 | }; 43 | 44 | return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4()); 45 | }; 46 | 47 | ///////// 48 | // Open MongoDB connection 49 | /////////////////////////// 50 | psm.db = mongoose.connect(psm.database.uri); 51 | psm.db.connection.on('error', function(err) { 52 | console.log('Could not connect to MongoDB.', err); 53 | process.exit(1); 54 | }); 55 | 56 | ///////// 57 | // Initialize 58 | /////////////////////////// 59 | psm.init = function(cb) { 60 | Config.findOne({}, function (err, doc) { 61 | if(err) { 62 | console.log('Unable to find PSM Configuration.'); 63 | console.log('Please check your database settings.'); 64 | if(cb) cb(err); 65 | return; 66 | } 67 | 68 | if(doc === null) { 69 | //insert default 70 | var cfg = new Config({ 71 | token: psm.generateGuid() 72 | }); 73 | cfg.save(function(err, def) { 74 | if(err) { 75 | if(cb) cb(err); 76 | return; 77 | } 78 | 79 | psm.config = def._doc; 80 | 81 | if(typeof(psm.config.api.port) != 'number') 82 | psm.config.api.port = parseInt(psm.config.api.port, 10); 83 | 84 | console.log('AUTH TOKEN:', psm.config.token); 85 | psm._init(cb); 86 | }); 87 | } else { 88 | psm.config = doc._doc; 89 | 90 | if(typeof(psm.config.api.port) != 'number') 91 | psm.config.api.port = parseInt(psm.config.api.port, 10); 92 | 93 | console.log('AUTH TOKEN:', psm.config.token); 94 | psm._init(cb); 95 | } 96 | }); 97 | }; 98 | 99 | psm._init = function(cb) { 100 | psm._configLogger(); 101 | psm._configCli(); 102 | psm._configSockets(); 103 | psm._configPackage(); 104 | psm._configManager(); 105 | 106 | cb(null); 107 | }; 108 | 109 | psm._configSockets = function() { 110 | //change sockets dir if this is windows 111 | if(process.platform === 'win32') { 112 | psm.config.sockets.dir = '\\\\.\\pipe\\'; 113 | } 114 | }; 115 | 116 | psm._configCli = function() { 117 | psm.cli = require('./psm/cli'); 118 | }; 119 | 120 | psm._configLogger = function() { 121 | psm.log = new lumber.Logger({ 122 | levels: { 123 | silent: -1, 124 | error: 0, 125 | warn: 1, 126 | info: 2, 127 | verbose: 3, 128 | debug: 4, 129 | silly: 5 130 | }, 131 | colors: { 132 | error: 'red', 133 | warn: 'yellow', 134 | info: 'cyan', 135 | verbose: 'magenta', 136 | debug: 'green', 137 | silly: 'rainbow' 138 | }, 139 | transports: [ 140 | new lumber.transports.File(psm.config.logging.file), 141 | new lumber.transports.Console(psm.config.logging.cli) 142 | ] 143 | }); 144 | }; 145 | 146 | psm._configPackage = function() { 147 | require('pkginfo')(module, 'version'); 148 | }; 149 | 150 | psm._configManager = function() { 151 | psm.manager = { 152 | cmd: function(cmd, server, data, cb) { 153 | if(typeof(data) == 'function') { 154 | cb = data; 155 | data = null; 156 | } 157 | 158 | if(typeof(server) == 'function') { 159 | cb = server; 160 | server = null; 161 | } 162 | 163 | var opts = { 164 | url: 'http://localhost:' + psm.config.api.port + 165 | '/' + cmd + (server ? '/' + server : '') + 166 | '?token=' + psm.config.token, 167 | method: (data ? 'POST' : 'GET'), 168 | json: data 169 | }; 170 | 171 | request(opts, function(err, res, body) { 172 | if(!err && res.statusCode == 200) { 173 | var json; 174 | try { 175 | json = JSON.parse(body); 176 | 177 | if(!json.success) { 178 | console.log(json.error); 179 | cb(new Error(json.error), json); 180 | } else { 181 | cb(null, json); 182 | } 183 | } catch(e) { 184 | cb(e, body); 185 | } 186 | } else { 187 | //if no error, then send a non-200 error, also send the 188 | //response for debugging if necessary 189 | cb((err ? 190 | err 191 | : 192 | new Error('Non 200 response code received.') 193 | ), res); 194 | } 195 | }); 196 | } 197 | }; 198 | }; 199 | 200 | //Modified from jQuery Core 201 | psm.proxy = function(fn, context) { 202 | var tmp, args, proxy; 203 | 204 | if (typeof context === "string") { 205 | tmp = fn[context]; 206 | context = fn; 207 | fn = tmp; 208 | } 209 | 210 | if (typeof(fn) != 'function') { 211 | return undefined; 212 | } 213 | 214 | args = Array.prototype.slice.call(arguments, 2); 215 | 216 | proxy = function () { 217 | return fn.apply(context, args.concat(Array.prototype.slice.call(arguments))); 218 | }; 219 | 220 | proxy.guid = fn.guid = fn.guid || proxy.guid || psm.guid(); 221 | 222 | return proxy; 223 | } 224 | 225 | psm.guid = function() { 226 | var S4 = function () { 227 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); 228 | }; 229 | 230 | return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4()); 231 | } -------------------------------------------------------------------------------- /lib/psm/manager/api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * api.js: provides RESTful api for the manager 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENCE 6 | * 7 | **/ 8 | 9 | var express = require('express'), 10 | os = require('os'), 11 | sio = require('socket.io'), 12 | //mongoose = require('mongoose'), 13 | //mongooseAuth = require('mongoose-auth'), 14 | psm = require('../psm'); 15 | 16 | var Api = exports.Api = function(manager) { 17 | var self = this; 18 | 19 | self._manager = manager; 20 | self._isRunning = false; 21 | 22 | self._configureServer(); 23 | self._setupRoutes(); 24 | }; 25 | 26 | Api.prototype.startup = function(cb) { 27 | var self = this; 28 | 29 | try { 30 | psm.log.debug('API Listening to ' + 31 | psm.config.api.host + ':' + 32 | psm.config.api.port); 33 | self._web.listen(psm.config.api.port, psm.config.api.host); 34 | self._isRunning = true; 35 | if(cb) cb(null); 36 | } catch(e) { 37 | psm.log.error('Error starting API Service.', e); 38 | if(cb) cb(e); 39 | } 40 | }; 41 | 42 | Api.prototype.shutdown = function() { 43 | var self = this; 44 | 45 | psm.log.silly('Attempting to shutdown API.'); 46 | if(self._isRunning) { 47 | psm.log.debug('Shutting down API.'); 48 | self._web.close(); 49 | self._isRunning = false; 50 | } 51 | }; 52 | 53 | Api.prototype._configureServer = function() { 54 | var self = this; 55 | 56 | self._web = express.createServer(); 57 | self._web.on('error', function(err) { 58 | psm.log.error('Error with API service.', err); 59 | }); 60 | 61 | self._io = sio.listen(self._web); 62 | 63 | self._web.configure(function() { 64 | psm.log.silly('Configuring server.'); 65 | self._web.use(express.bodyParser()); 66 | self._web.enable('jsonp callback'); 67 | }); 68 | 69 | self._io.configure(function() { 70 | psm.log.silly('Configuring Socket.IO'); 71 | self._io.set('authorization', function(hs, accept) { 72 | if(hs.query.token == psm.config.token) 73 | return accept(null, true); 74 | 75 | return accept('Invalid auth token.', false); 76 | }); 77 | }); 78 | }; 79 | 80 | Api.prototype._setupRoutes = function() { 81 | var self = this; 82 | 83 | self._web.get('/*', function(req, res, next) { 84 | //res.header('Access-Control-Allow-Origin', '*'); 85 | //res.header('Access-Control-Allow-Method', 'POST, GET, PUT, OPTIONS'); 86 | //res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers']); 87 | 88 | psm.log.silly('Incoming request...'); 89 | if(req.query.token == psm.config.token) 90 | next(); 91 | else 92 | res.json({ success: false, error: 'Invalid auth token.' }); 93 | }); 94 | 95 | //manager commands 96 | self._web.get('/:server/:cmd(start|stop|list)', function(req, res) { 97 | var cmd = req.params.cmd, 98 | server = req.params.server; 99 | 100 | psm.log.debug('Got command: %s, for server: %s', cmd, server); 101 | 102 | if(cmd == 'start') cmd = 'startServer'; 103 | 104 | self._manager[cmd](server, function(err, data) { 105 | if(err) psm.log.error(err, 'Error executing command: %s', cmd); 106 | 107 | res.json({ success: !err, error: (err ? err.message : null), data: data }); 108 | }); 109 | }); 110 | 111 | //worker commands 112 | self._web.get(/^(\/[\w\-]+){3,}?/, function(req, res) { 113 | var args = req.url.split('/').splice(1), 114 | server = args[0], helper = args[1], cmd = args[2], 115 | p; 116 | 117 | //take off the 3 main parts (server, helper, cmd) 118 | args.splice(0, 3); 119 | 120 | //remove last entry if it is '' (happens on trailing slash) 121 | if((p = args.pop()) != '') args.push(p); 122 | 123 | //what is left in 'args' are the arguments for the function 124 | if(cmd.charAt(0) == '_') { 125 | res.json({ success: false, error: 'Unable to call private command.', data: null }); 126 | } else { 127 | self._manager._workerCmd(server, cmd, helper, args, function(err, data) { 128 | res.json({ success: !err, error: (err ? err.message : null), data: data }); 129 | }); 130 | } 131 | }); 132 | 133 | //actual command to server 134 | self._web.post('/:server/cmd', function(req, res) { 135 | psm.log.debug(req.body, 'Got console command for server: %s', req.params.server); 136 | 137 | self._manager._workerCmd(req.params.server, 'cmd', 'system', req.body.cmd, function(err, data) { 138 | //self._manager.cmd(req.body.cmd, req.params.server, function(err, data) { 139 | res.json({ success: !err, error: (err ? err.message : null), data: data }); 140 | }); 141 | }); 142 | 143 | //Daemon Status 144 | self._web.get('/system/status', function(req, res) { 145 | res.json({ 146 | success: true, 147 | status: { 148 | hostname: os.hostname(), 149 | type: os.type(), 150 | arch: os.arch(), 151 | loadavg: os.loadavg(), 152 | freemem: os.freemem(), 153 | totalmem: os.totalmem(), 154 | cpus: os.cpus() 155 | } 156 | }); 157 | }); 158 | 159 | //Get Configuration 160 | self._web.get('/config/:key', function(req, res) { 161 | psm.log.debug('Got config request, key: %s', req.params.key); 162 | 163 | var levels = req.params.key.split('.'), 164 | opt = psm.config, 165 | i = levels.length - 1, 166 | key; 167 | 168 | while (i--) opt = opt[levels.shift()]; 169 | key = levels.shift(); 170 | 171 | res.json({ success: true, value: opt[key] }); 172 | }); 173 | 174 | //Update Configuration 175 | self._web.post('/config/:key', function(req, res) { 176 | psm.log.debug('Got config request, key: %s', req.params.key); 177 | 178 | var levels = req.params.key.split('.'), 179 | opt = psm.config, 180 | i = levels.length - 1, 181 | body, key; 182 | 183 | while (i--) opt = opt[levels.shift()]; 184 | key = levels.shift(); 185 | 186 | try { 187 | body = JSON.parse(req.body); 188 | } catch(e) { 189 | res.json({ success: false, error: e.message }); 190 | return; 191 | } 192 | 193 | opt[key] = body; 194 | 195 | res.json({ success: true, value: opt[key] }); 196 | 197 | }); 198 | 199 | //Add/Remove servers 200 | self._web.get('/server/rm/:server', function(req, res) { 201 | psm.log.debug('Got request to remove server: %s', req.params.server); 202 | 203 | self._manager.rmServer 204 | }); 205 | 206 | self._web.post('/server/add', function(req, res) { 207 | var body = req.body; 208 | 209 | psm.log.debug(body, 'Got request to %s server', cmd); 210 | 211 | if(typeof(body) == 'string') { 212 | try { body = JSON.parse(body); } 213 | catch(e) { 214 | psm.log.error(e, 'Bad JSON given from worker %s', server); 215 | res.json({ success: false, error: e.message }); 216 | return; 217 | } 218 | } 219 | 220 | self._manager.addServer(body, function(err) { 221 | if(err) psm.log.error(err, 'Error trying to add server'); 222 | 223 | res.json({ success: !err, error: (err ? err.message : null) }); 224 | }); 225 | }); 226 | }; -------------------------------------------------------------------------------- /lib/psm/manager/manager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * manager.js: manages local workers 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENCE 6 | * 7 | **/ 8 | 9 | var path = require('path'), 10 | Hook = require('hook.io').Hook, 11 | fs = require('fs'), 12 | mongoose = require('mongoose'), 13 | utile = require('utile'), 14 | //Worker = require('../worker/worker').Worker, 15 | Api = require('./api').Api, 16 | psm = require('../../psm'); 17 | 18 | var Manager = exports.Manager = function(options) { 19 | var self = this; 20 | 21 | options.verbose = true; 22 | 23 | Hook.call(self, options); 24 | 25 | self._workerScript = path.join(__dirname, '..', 'worker', 'worker.js'); 26 | self._reservedServerNames = [ 27 | 'server', 'config', 'system' 28 | ]; 29 | 30 | self._servers = {}; 31 | self._workers = {}; 32 | }; 33 | 34 | utile.inherits(Manager, Hook); 35 | 36 | Manager.prototype.startup = function(cb) { 37 | var self = this; 38 | 39 | //load servers from MongoDB 40 | self._loadServers(function(err) { 41 | //start RESTful API server 42 | self._startApi(function(err) { 43 | if(err) { if(cb) cb(err); return; } 44 | 45 | psm.log.silly('API started!'); 46 | //startup completed 47 | if(cb) cb(null); 48 | }); 49 | }); 50 | }; 51 | 52 | //isn't named 'start' because that would shadow the 53 | //base Hook.start() function 54 | Manager.prototype.startServer = function(server, cb) { 55 | var self = this; 56 | 57 | self._startWorker(server, function(err) { 58 | console.log('WORKER STARTED', err); 59 | if(err) { if(cb) cb(err); return; } 60 | 61 | self._workerCmd('start', server, cb); 62 | }); 63 | }; 64 | 65 | Manager.prototype.stop = function(server, cb) { 66 | var self = this; 67 | 68 | self._workerCmd('stop', server, function() { 69 | self.kill(server, function() { 70 | self._workers[server] = null; 71 | if(cb) cb(); 72 | }); 73 | }); 74 | }; 75 | 76 | Manager.prototype.list = function(cb) { 77 | var self = this; 78 | 79 | utile.each(self._servers, function(server, sid) { 80 | server.running = !!self._workers[sid]; 81 | }); 82 | 83 | if(cb) cb(null, self._servers); 84 | }; 85 | 86 | Manager.prototype.addServer = function(server, cb) { 87 | var self = this; 88 | 89 | if(self._reservedServerNames.indexOf(server.name) !== -1) 90 | cb(new Error(server.name + ' is a reserved name, please choose something else.')); 91 | else { 92 | self._servers[server.name] = server; 93 | self._storeServers(cb); 94 | } 95 | }; 96 | 97 | Manager.prototype.rmServer = function(server, cb) { 98 | var self = this; 99 | 100 | if(self._workers[server]) { 101 | if(cb) cb(new Error('This server is running, please shut it down before removing it.')); 102 | } else { 103 | psm.log.debug('Deleting server from memory list'); 104 | 105 | delete self._servers[server]; 106 | self._storeServers(); 107 | 108 | if(cb) cb(); 109 | } 110 | }; 111 | 112 | Manager.prototype._onWorkerReady = function(server, cb) { 113 | var self = this; 114 | 115 | psm.log.silly('Removing spawn error listener for worker hook.'); 116 | self.removeListener('hook::spawn::error', self._workers[server].onError); 117 | 118 | psm.log.debug('Worker hook ready, starting up...'); 119 | self._workerCmd('startup', server, self._servers[server], function() { 120 | self._workers[server].ready = true; 121 | if(cb) cb(); 122 | }); 123 | }; 124 | 125 | //little backwards on the params due to the proxy method 126 | Manager.prototype._onWorkerError = function(server, cb, err) { 127 | var self = this; 128 | 129 | psm.log.error(err, 'Error from worker process!'); 130 | self._workers[server] = null; 131 | 132 | if(cb) cb(err); 133 | }; 134 | 135 | Manager.prototype._workerCmd = function(server, method, man, args, cb) { 136 | var self = this; 137 | 138 | if(typeof(args) == 'function') { 139 | cb = args; 140 | args = []; 141 | } 142 | 143 | if(!self._workers[server]) { 144 | if(cb) cb(new Error('No worker process started for server "' + server + '"')); 145 | } else { 146 | self.emit('action::' + server, { 147 | method: method, 148 | man: man, 149 | args: args 150 | }, cb); 151 | } 152 | }; 153 | 154 | Manager.prototype._setupWorkerListeners = function() { 155 | self.on('*::event::output', function(data) { 156 | self._api._io.sockets.emit('output', data); 157 | }); 158 | 159 | self.on('*::event::player::connect', function(data) { 160 | self._api._io.sockets.emit('player::connect', data); 161 | }); 162 | 163 | self.on('*::event::player::disconnect', function(data) { 164 | self._api._io.sockets.emit('player::disconnect', data); 165 | }); 166 | 167 | self.on('*::event::player::chat', function(data) { 168 | self._api._io.sockets.emit('player::chat', data); 169 | }); 170 | 171 | self.on('*::event::mcwarn', function(data) { 172 | self._api._io.sockets.emit('mcwarn', data); 173 | }); 174 | 175 | self.on('*::event::mcerror', function(data) { 176 | self._api._io.sockets.emit('mcerror', data); 177 | }); 178 | }; 179 | 180 | Manager.prototype._startApi = function(cb) { 181 | var self = this; 182 | 183 | psm.log.debug('Initializing API'); 184 | self._api = new Api(self); 185 | 186 | psm.log.debug('Starting API'); 187 | self._api.startup(cb); 188 | }; 189 | 190 | Manager.prototype._loadServers = function(cb) { 191 | var self = this, 192 | Server = mongoose.model('Server'); 193 | 194 | psm.log.debug('Loading servers from MongoDB.'); 195 | Server.find({}, function(err, docs) { 196 | if(err) { 197 | psm.log.error(err, 'Unable to load servers from MongoDB.'); 198 | if(cb) cb(err); 199 | return; 200 | } 201 | 202 | docs.forEach(function(doc) { 203 | psm.log.silly(doc._doc, 'Server loaded.'); 204 | self._servers[doc._doc.name] = doc._doc; 205 | }); 206 | 207 | if(cb) cb(null); 208 | }); 209 | }; 210 | 211 | Manager.prototype._storeServers = function(cb) { 212 | var self = this, 213 | Server = mongoose.model('Server'), 214 | done = 0, 215 | len = Object.keys(self._servers).length, 216 | errors = []; 217 | 218 | psm.log.debug('Storing servers currently loaded in memory'); 219 | utile.each(self._servers, function(server, name) { 220 | psm.log.silly('Upserting server %s', name); 221 | Server.update({ name: name }, server, { upsert: true }, function(err) { 222 | if(err) errors.push(err); 223 | 224 | done++; 225 | if(done == len) { 226 | if(errors.length && cb) cb(errors); 227 | else if(cb) cb(null); 228 | } 229 | }); 230 | }); 231 | }; 232 | 233 | Manager.prototype._startWorker = function(server, cb) { 234 | var self = this; 235 | 236 | if(self._workers[server]) { 237 | //Already started 238 | if(cb) cb(); 239 | } else if(!self._servers[server]) { 240 | //Unknown server 241 | if(cb) cb(new Error('Unknown server "' + server + '", please add this server first')); 242 | } else { 243 | psm.log.silly('Setting up a new worker object'); 244 | self._workers[server] = { 245 | server: server, 246 | ready: false, 247 | pidFile: psm.config.pids.worker.replace('$#', server), 248 | onError: psm.proxy(self._onWorkerError, self, server, cb), 249 | onReady: psm.proxy(self._onWorkerReady, self, server, cb) 250 | }; 251 | 252 | psm.log.debug('Spawning worker hook'); 253 | 254 | self.spawn({ 255 | name: server, 256 | src: self._workerScript 257 | }); 258 | 259 | self.once('hook::spawn::error', self._workers[server].onError); 260 | self.once('children::ready', self._workers[server].onReady); 261 | } 262 | }; -------------------------------------------------------------------------------- /lib/psm/worker/helpers/output.js: -------------------------------------------------------------------------------- 1 | var events = require('events'), 2 | colors = require('colors'), 3 | util = require('util'); 4 | 5 | var OutputParser = exports.OutputParser = function(options) { 6 | var self = this; 7 | events.EventEmitter.call(self); 8 | 9 | self.log = options.logger; 10 | self.worker = options.worker; 11 | self.server = options.server; 12 | 13 | self._players = []; 14 | self._version = ''; 15 | self._parsers = self._getParsers(); 16 | 17 | self._defineGetters(); 18 | self._defineSetters(); 19 | }; 20 | 21 | util.inherits(OutputParser, events.EventEmitter); 22 | 23 | OutputParser.prototype._addParser = function(fn) { 24 | return (self._parsers.push(fn) - 1); 25 | }; 26 | 27 | OutputParser.prototype._removeParser = function(id) { 28 | self._parsers.splice(id, 1); 29 | }; 30 | 31 | OutputParser.prototype._defineGetters = function() { 32 | var self = this; 33 | 34 | self.__defineGetter__('mcversion', function() { 35 | return self._mcversion; 36 | }); 37 | 38 | self.__defineGetter__('cbversion', function() { 39 | return self._cbversion; 40 | }); 41 | }; 42 | 43 | OutputParser.prototype._defineSetters = function() { 44 | var self = this; 45 | 46 | self.__defineSetter__('stream', function(strm) { 47 | if(self._outputWrap) 48 | self._stream.removeListener('data', self._outputWrap); 49 | 50 | self._outputWrap = function() { self._parseOutput.apply(self, arguments); }; 51 | 52 | strm.on('data', self._outputWrap); 53 | self._stream = strm; 54 | }); 55 | }; 56 | 57 | OutputParser.prototype._parseOutput = function(output) { 58 | var self = this, 59 | strs = output.toString().trim().split('\n'); 60 | 61 | self.worker.proc.resetIdle(); 62 | 63 | self.emit('output', strs); 64 | 65 | strs.forEach(function(str) { 66 | self._parsers.forEach(function(parser) { 67 | parser.call(self, str); 68 | }); 69 | }); 70 | }; 71 | 72 | OutputParser.prototype._getParsers = function() { 73 | return [ 74 | function(str) { 75 | //player connect 76 | var parts = str.match(/^([0-9\-: ]+) \[INFO\] ([^\s]+) \[\/([\d\.:]+)\] logged in with entity id ([\d]+) at \((\[([^\s]+)\] )?([\d\.\-\, ]+)\)$/); 77 | 78 | if(parts) { 79 | //0 = entire message, 80 | //1 = timestamp, 81 | //2 = player name, 82 | //3 = IP:Port 83 | //4 = entity id 84 | //5 = [worldname] 85 | //6 = worldname 86 | //7 = location logged into 87 | this.log.debug('Player connected: %s', parts[2]); 88 | this.emit('player::connect', parts[1], { 89 | connect: parts[1], 90 | name: parts[2], 91 | ip: parts[3], 92 | id: parts[4], 93 | world: parts[6], 94 | loginLoc: parts[7] 95 | }); 96 | } 97 | }, 98 | function(str) { 99 | //player disconnect 100 | var i, player, 101 | parts = str.match(/^([0-9\-: ]+) \[INFO\] ([^\s]+) lost connection: ([\w\. ]+)$/); 102 | 103 | if(parts) { 104 | //0 = entire message, 105 | //1 = timestamp, 106 | //2 = player name, 107 | //3 = reason 108 | this.log.debug('Player disconnected: %s', parts[2]); 109 | this.emit('player::disconnect', parts[1], parts[2]); 110 | } 111 | }, 112 | function(str) { 113 | //chat message 114 | str = str.stripColors; 115 | var parts = str.match(/^([0-9\-: ]+) \[INFO\] <([^>]+)> (.*)$/); 116 | 117 | if(parts) { 118 | //0 = entire msg, 119 | //1 = timestamp, 120 | //2 = player name, 121 | //3 = message 122 | this.log.silly('Player %s chatted: %s', parts[2], parts[3]); 123 | this.emit('player::chat', parts[1], parts[2], parts[3]); 124 | } 125 | }, 126 | function(str) { 127 | //server startup 128 | var parts = str.match(/^([0-9\-: ]+) \[INFO\] Done \(([0-9\.s]+)\)!/); 129 | 130 | if(parts) { 131 | //0 = entire message, 132 | //1 = timestamp, 133 | //2 = startup time 134 | this.log.info('Server has started up (' + parts[2] + ')'); 135 | this.emit('startup::done', parts[1]); 136 | } 137 | }, 138 | function(str) { 139 | //errors 140 | var parts = str.match(/^([0-9\-: ]+) \[(SEVERE|WARNING|FATAL)\] (.*)$/); 141 | 142 | if(parts) { 143 | //0 = entire message, 144 | //1 = timestamp, 145 | //2 = message type, 146 | //3 = message 147 | 148 | //I have NO IDEA why this is just a warning... 149 | if(parts[2] == 'WARNING' && parts[3] == '**** FAILED TO BIND TO PORT!') { 150 | //we need to log the error and kill the server 151 | this.log.error('Server unable to start: %s', parts[3]); 152 | this.emit('startup::fail', new Error(parts[3])); 153 | } else if(parts[2] == 'WARNING') { 154 | this.log.warn(parts[0]); 155 | this.emit('mcwarn', parts[0]); 156 | } else { 157 | this.log.error(parts[0]); 158 | this.emit('mcerror', parts[0], parts[2]); 159 | } 160 | } 161 | }, 162 | function(str) { 163 | //log when generating map 164 | var parts = str.match(/^([0-9\-: ]+) \[INFO\] Preparing (level|spawn area:) ([\w\"\d%]+)$/); 165 | 166 | if(parts) { 167 | //0 = entire message, 168 | //1 = timestamp, 169 | //2 = level OR spawn area: 170 | //3 = "world_name" OR #% 171 | this.log.info(parts[0].replace(parts[1] + ' [INFO] ', '')); 172 | if(parts[2] == 'level') { 173 | this.emit('map::prepare', { world: parts[3] }); 174 | } else { 175 | this.emit('map::progress', { progress: parts[3] }); 176 | } 177 | } 178 | }, 179 | function(str) { 180 | //version string 181 | var parts = str.match(/^([0-9\-: ]+) \[INFO\] Starting minecraft server version ([\d\.]+)$/); 182 | 183 | if(parts) { 184 | //0 = entire message, 185 | //1 = timestamp, 186 | //2 = version 187 | this._mcversion = parts[2]; 188 | } 189 | }, 190 | function(str) { 191 | //cb version string 192 | var parts = str.match(/^([0-9\-: ]+) \[INFO\] This server is running CraftBukkit version git-Bukkit-([\d\.]+)(-R\d\.\d)?-b([\d]{4}).+$/); 193 | 194 | if(parts) { 195 | //0 = entire message, 196 | //1 = timestamp, 197 | //2 = mc version, 198 | //3 = release version, 199 | //4 = build version 200 | this._cbversion = parts[4]; 201 | } 202 | }, 203 | function(str) { 204 | //cb update 205 | var update = str.match(/^([0-9\-: ]+) \[WARNING\] Your version of CraftBukkit is out of date. Version ([\d\.\-R]+) \(build #([\d]+)\) was released on (.+)$/), 206 | details = str.match(/^([0-9\-: ]+) \[WaRNING\] Details: (.+)$/), 207 | download = str.match(/^([0-9\-: ]+) \[WARNING\] Download: (.+)$/); 208 | 209 | if(update) { 210 | //0 = entire message, 211 | //1 = timestamp, 212 | //2 = mc version, 213 | //3 = build version 214 | this._update = { 215 | version: update[3] 216 | }; 217 | } else if(details) { 218 | //0 = entire message, 219 | //1 = timestamp, 220 | //2 = url 221 | this._update.details = details[2]; 222 | } else if(download) { 223 | //0 = entire message, 224 | //1 = timestamp, 225 | //2 = url 226 | this._update.download = download[2]; 227 | } 228 | } 229 | ]; 230 | }; -------------------------------------------------------------------------------- /lib/psm/worker/worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * worker.js: manages a single local gameserver instance 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENCE 6 | * 7 | **/ 8 | 9 | var psm = require('../../psm'), 10 | Hook = require('hook.io').Hook, 11 | utile = require('utile'), 12 | fs = require('fs'), 13 | events = require('events'), 14 | helpers = utile.requireDir('./helpers'); 15 | 16 | var Worker = exports.Worker = function(options) { 17 | var self = this; 18 | 19 | Hook.call(self, options); 20 | 21 | self._serverId = self.name; 22 | 23 | self._setupActionListeners(); 24 | }; 25 | 26 | utile.inherits(Worker, Hook); 27 | 28 | //These functions have an unused 'n' param due to the way 29 | //the manager calls these functions. The 'n' slot is reserved 30 | //for extra data, and if their is none then 'null' is sent. 31 | //The 'n' consumes the 'null' so the cb falls in the right place. 32 | Worker.prototype.startServer = function(cb) { 33 | var self = this; 34 | 35 | psm.log.debug('Starting server ' + self._serverId); 36 | psm.log.silly('Checking world links...'); 37 | self.files.checkWorldLinks(function(err) { 38 | if(err) { 39 | psm.log.error(err, 'Failed to check world links.'); 40 | if(cb) cb(err); 41 | return; 42 | } 43 | 44 | psm.log.silly('Moving worlds to RAM'); 45 | self.files.worldsToRam(function(err) { 46 | if(err) { 47 | psm.log.error(err, 'Failed to move worlds to RAM disk.'); 48 | if(cb) cb(err); 49 | return; 50 | } 51 | 52 | psm.log.silly('Starting server'); 53 | self.proc.start(function(err, proc) { 54 | if(err) { 55 | psm.log.error(err, 'Unable to start minecraft process.'); 56 | if(cb) cb(err); 57 | return; 58 | } 59 | 60 | psm.log.silly('Done.'); 61 | if(cb) cb(); 62 | }); 63 | }); 64 | }); 65 | }; 66 | 67 | Worker.prototype.stop = function(cb) { 68 | var self = this; 69 | 70 | psm.log.debug('Stopping server ' + self._serverId); 71 | psm.log.silly('Stopping server process'); 72 | self.proc.stop(function(err) { 73 | if(err) { 74 | psm.log.error(err, 'Failed to stop minecraft process.'); 75 | if(cb) cb(err); 76 | return; 77 | } 78 | 79 | psm.log.silly('Moving world files to disk'); 80 | self.files.worldsToDisk(function(err) { 81 | if(err) { 82 | psm.log.error(err, 'Failed to move world files to disk.'); 83 | if(cb) cb(err); 84 | return; 85 | } 86 | 87 | psm.log.silly('Done.'); 88 | if(cb) cb(); 89 | }); 90 | }); 91 | }; 92 | 93 | Worker.prototype.restart = function(cb) { 94 | var self = this; 95 | 96 | psm.log.debug('Restarting server ' + self._serverId); 97 | psm.log.silly('Stopping server process'); 98 | self.proc.stop(function(err) { 99 | if(err) { 100 | psm.log.error(err, 'Failed to stop minecraft process.'); 101 | if(cb) cb(err); 102 | return; 103 | } 104 | 105 | psm.log.silly('Done.'); 106 | psm.log.silly('Moving world files to disk'); 107 | self.proc.start(function(err, proc) { 108 | if(err) { 109 | psm.log.error(err, 'Failed to start minecraft process.'); 110 | if(cb) cb(err); 111 | return; 112 | } 113 | 114 | if(cb) cb(); 115 | }); 116 | }); 117 | }; 118 | 119 | Worker.prototype.status = function(cb) { 120 | var self = this, 121 | status = { 122 | running: self.proc.running, 123 | players: self.outputs.players, 124 | properties: self.files.properties, 125 | mcversion: self.outputs.mcversion, 126 | cbversion: self.outputs.cbversion 127 | }; 128 | 129 | if(cb) cb(null, status); 130 | }; 131 | 132 | Worker.prototype.cmd = function(cmd, cb) { 133 | var self = this; 134 | 135 | if(cmd instanceof Array) { 136 | cmd = cmd.join(' '); 137 | } 138 | 139 | self.proc.input(cmd + '\n', function(err) { 140 | if(err) { psm.log.error(err, 'Unable to do command ' + cmd); } 141 | 142 | if(cb) cb(err); 143 | }); 144 | }; 145 | 146 | Worker.prototype.update = function(n, cb) { 147 | this.files.update(cb); 148 | }; 149 | 150 | Worker.prototype.backupServer = function(n, cb) { 151 | this.files.backupServer(cb); 152 | }; 153 | 154 | Worker.prototype.backupMaps = function(n, cb) { 155 | this.files.backupMaps(cb); 156 | }; 157 | 158 | Worker.prototype.backupLogs = function(n, cb) { 159 | this.file.backupLogs(cb); 160 | }; 161 | 162 | Worker.prototype.reloadConfig = function(n, cb) { 163 | this._getSettings(cb); 164 | }; 165 | 166 | Worker.prototype.isRunning = function(n, cb) { 167 | if(cb) cb(null, this.proc.running); 168 | 169 | return this.proc.running; 170 | }; 171 | 172 | Worker.prototype.startup = function(server, cb) { 173 | var self = this; 174 | 175 | try { 176 | self.server = server = JSON.parse(server); 177 | } catch(e) { 178 | psm.log.error(e, 'Unable to startup worker process.'); 179 | } 180 | 181 | psm.init(function() { 182 | var args = { 183 | logger: psm.log, 184 | worker: self, 185 | server: server 186 | }; 187 | 188 | psm.log.silly('Instantiating Helpers'); 189 | 190 | self.outputs = new helpers.output.OutputParser(args); 191 | self._setupOutputListeners(); 192 | 193 | self.files = new helpers.files.FileManager(args); 194 | self.proc = new helpers.proc.ProcManager(args); 195 | self.jobs = new helpers.jobs.JobManager(args); 196 | self.logs = new helpers.logs.LogManager(args); 197 | self.players = new helpers.players.PlayerManager(args); 198 | self.plugins = new helpers.plugins.PluginManager(args); 199 | self.system = new helpers.system.SystemManager(args); 200 | self.worlds = new helpers.worlds.WorldManager(args); 201 | 202 | if(cb) cb(); 203 | }); 204 | }; 205 | 206 | Worker.prototype._notifyEvent = function(event, data, cb) { 207 | var self = this; 208 | 209 | if(typeof(data) == 'function') { 210 | cb = data; 211 | data = {}; 212 | } 213 | 214 | data.server = self._serverId; 215 | 216 | self.emit('event::' + event, data, cb); 217 | }; 218 | 219 | Worker.prototype._setupActionListeners = function() { 220 | var self = this;/*, 221 | actions = [ 222 | 'startup', 223 | 'start', 'stop', 'restart', 'status', 224 | 'cmd', 'update', 225 | 'backupServer', 'backupMaps', 'backupLogs', 226 | 'reloadConfig', 'isRunning' 227 | ]; 228 | 229 | actions.forEach(function(act) { 230 | //the ternary is for calling startServer when act is start 231 | self.on('action::' + self._serverId + '::' + act, 232 | psm.proxy(self[act == 'start' ? 'startServer' : act], self)); 233 | });*/ 234 | 235 | self.on('action::' + self._serverId, function(data, cb) { 236 | if(!data.man || !self[data.man]) { 237 | if(cb) cb('Bad manager.'); 238 | return; 239 | } 240 | 241 | if(!data.method || !self[data.man][data.method]) { 242 | if(cb) cb('Bad method.'); 243 | return; 244 | } 245 | 246 | data.args.push(cb); 247 | self[data.man][data.method].apply(data.args, self[data.man]); 248 | }); 249 | }; 250 | 251 | Worker.prototype._setupOutputListeners = function() { 252 | var self = this; 253 | 254 | self.outputs.on('mcerror', function(message, type) { 255 | //hmmm...had a MC error of some kind 256 | }); 257 | 258 | self.outputs.on('mcwarn', function(message, type) { 259 | //hmmm...had a MC warning 260 | }); 261 | 262 | self.outputs.on('output', function(lines) { 263 | self._notifyEvent('output', { lines: lines }); 264 | }); 265 | 266 | self.outputs.on('player::connect', function(timestamp, name) { 267 | self._notifyEvent('player::connect', { timestamp: timestamp, name: name }); 268 | }); 269 | 270 | self.outputs.on('player::disconnect', function(timestamp, name) { 271 | self._notifyEvent('player::disconnect', { timestamp: timestamp, name: name }); 272 | }); 273 | 274 | self.outputs.on('player::chat', function(timestamp, name, msg) { 275 | self._notifyEvent('player::chat', { timestamp: timestamp, name: name, msg: msg }); 276 | }); 277 | }; -------------------------------------------------------------------------------- /lib/psm/worker/helpers/worlds.js: -------------------------------------------------------------------------------- 1 | var utile = require('utile'), 2 | events = require('events'), 3 | cp = require('child_process'), 4 | path = require('path'), 5 | request = require('request'), 6 | Job = require('./job').Job; 7 | 8 | var WorldManager = exports.WorldManager = function(options) { 9 | var self = this; 10 | events.EventEmitter.call(self); 11 | options = options || {}; 12 | 13 | self.log = options.logger; 14 | self.worker = options.worker; 15 | self.server = options.server; 16 | 17 | self.paths = options.server.paths; 18 | 19 | self._defineGetters(); 20 | self._defineSetters(); 21 | }; 22 | 23 | utile.inherits(WorldManager, events.EventEmitter); 24 | 25 | WorldManager.prototype.get = function(world, cb) { 26 | var self = this; 27 | 28 | if(world) { 29 | //TODO: Get specific info on a world 30 | cb(new Error('Not yet implemented.')); 31 | } else { 32 | //TODO: add regex/func to filter for only real worlds? 33 | //check for level.dat inside folder if it is a real world? 34 | self.worker.files.listDirs(self.paths.worldDisk, cb); 35 | } 36 | }; 37 | 38 | WorldManager.prototype.backup = function(w, cb) { 39 | var self = this, 40 | worlds = ['world', 'world_nether', 'world_the_end'], 41 | errors = [], 42 | cmds = []; 43 | 44 | if(typeof(w) == 'function') { 45 | cb = w; 46 | w = null; 47 | } 48 | 49 | //ensure backup path exists 50 | utile.mkdirp(self.backup.maps.path, function(err) { 51 | if(err) { if(cb) cb(err); return; } 52 | 53 | //setup commands 54 | if(w) { 55 | cmds.push(self._buildBackupCommand(w)); 56 | } else { 57 | worlds.forEach(function(world) { 58 | cmds.push(self._buildBackupCommand(world)); 59 | }); 60 | }); 61 | 62 | //run each async 63 | async.parallel(cmds, function(err, results) { 64 | if(err) errors.push(err); 65 | 66 | if(results) { //all done 67 | if(cb) cb(errors.length ? errors : null); 68 | } 69 | }); 70 | }); 71 | }; 72 | 73 | WorldManager.prototype.restore = function(world, file, cb) { 74 | var self = this; 75 | }; 76 | 77 | WorldManager.prototype.toDisk = function(cb) { 78 | var self = this, 79 | worldRam = self.paths.worldRam, 80 | worldDisk = self.paths.worldDisk, 81 | ramWorlds = self.ramWorlds, 82 | worlds = self.worlds, 83 | cmds = [], 84 | errors = []; 85 | 86 | if(worlds.length === 0) { 87 | if(cb) cb(); 88 | return; 89 | } 90 | 91 | //self.savesToggle(false); 92 | 93 | //setup each copy command 94 | worlds.forEach(function(world) { 95 | if(ramWorlds.indexOf(world) != -1) { 96 | cmds.push(function(next) { 97 | utile.cpr(path.join(worldRam, world), path.join(worldDisk, world), next); 98 | }); 99 | } 100 | }); 101 | 102 | //run each in parallel 103 | async.parallel(cmds, function(err, results) { 104 | if(err) errors.push(err); 105 | 106 | if(results) { //all done 107 | if(cb) cb(errors.length ? errors : null); 108 | } 109 | }); 110 | 111 | /* 112 | utile.rimraf(path.join(worldRam, world), function(err) { 113 | if(err) errors.push(err); 114 | 115 | done++; 116 | if(done == worlds.length) { 117 | if(errors.length && cb) cb(errors); 118 | else if(cb) cb(null); 119 | } 120 | }); 121 | */ 122 | 123 | //self.savesToggle(true); 124 | }; 125 | 126 | WorldManager.prototype.toRam = function(cb) { 127 | var self = this, 128 | worldRam = self.paths.worldRam, 129 | worldDisk = self.paths.worldDisk, 130 | ramWorlds = self.ramWorlds, 131 | worlds = self.worlds, 132 | cmds = [], 133 | errors = []; 134 | 135 | if(worlds.length === 0) { 136 | if(cb) cb(); 137 | return; 138 | } 139 | 140 | //setup copy commands 141 | worlds.forEach(function(world) { 142 | if(ramWorlds.indexOf(world) != -1) { 143 | cmds.push(function(next) { 144 | utile.cpr(path.join(worldDisk, world), path.join(worldRam, world), next); 145 | }); 146 | } 147 | }); 148 | 149 | //run each in parallel 150 | async.parallel(cmds, function(err, results) { 151 | if(err) errors.push(err); 152 | 153 | if(results) { //all done 154 | if(cb) cb(errors.length ? errors : null); 155 | } 156 | }); 157 | }; 158 | 159 | WorldManager.prototype.checkLinks = function(cb) { 160 | var self = this, 161 | bin = self.paths.bin, 162 | worldRam = self.paths.worldRam, 163 | worldDisk = self.paths.worldDisk, 164 | ramWorlds = self.ramWorlds, 165 | worlds = self.worlds, 166 | cmds = [], 167 | errors = []; 168 | 169 | //only need links if we store worlds somewhere 170 | //other than the main mc folder 171 | if(bin == worldDisk || worlds.length === 0) { if(cb) cb(); return; } 172 | 173 | self.log.debug('Ensuring world disk location exists'); 174 | utile.mkdirp(worldDisk, function(err) { 175 | self.log.silly('iterating through each world'); 176 | async.forEach(worlds, function(world, next) { 177 | self.log.silly('Checking links for world: %s', world); 178 | self._checkWorldLink(world, function(err) { 179 | if(err) errors.push(err); 180 | 181 | self.log.silly('Links checked for world: %s', world); 182 | next(); 183 | }); 184 | }, function(err) { 185 | if(cb) cb(errors.length ? errors : null); 186 | }); 187 | }); 188 | }; 189 | 190 | WorldManager.prototype._checkLink = function(world, cb) { 191 | var self = this, 192 | l, isL, lLoc, 193 | bin = self.paths.bin, 194 | worldRam = self.paths.worldRam, 195 | worldDisk = self.paths.worldDisk, 196 | ramWorlds = self.ramWorlds; 197 | 198 | try { 199 | l = fs.lstatSync(path.join(bin, world)); 200 | isL = l.isSymbolicLink(); 201 | lLoc = fs.readlinkSync(path.join(bin, world)); 202 | } catch(e) {} 203 | 204 | //if is a link or doesn't exist 205 | self.log.silly('Checking link for world: %s, isSymlink: %s, exists: %s', world, !!isL, !!l); 206 | if(isL || !l) { 207 | if(ramWorlds.indexOf(world) != -1) { 208 | //create the worldram location 209 | self.log.silly('Creating worldRam location for %s', world); 210 | self._doCreateLoc(worldRam, world, l, lLoc, function(err) { 211 | if(cb) cb(err); 212 | }); 213 | } else { 214 | //create disk location 215 | self.log.silly('Creating worldDisk location for %s', world); 216 | self._doCreateLoc(worldDisk, world, l, lLoc, function(err) { 217 | if(cb) cb(err); 218 | }); 219 | } 220 | } else { 221 | //this is a real world-folder, lets move it to worldstorage 222 | self.log.info('Moving %s files to world disk location', world); 223 | utile.cpr(path.join(bin, world), path.join(worldDisk, world), function(err) { 224 | if(err) { if(cb) cb(err); return; } 225 | 226 | //now that we moved it lets check again 227 | self._checkWorldLink(world, cb); 228 | }); 229 | } 230 | }; 231 | 232 | WorldManager.prototype._doCreateLoc = function(p, world, l, lLoc, cb) { 233 | var self = this, 234 | bin = self.paths.bin, 235 | worldLoc = path.join(p, world); 236 | 237 | utile.mkdirp(worldLoc, function(err) { 238 | //ensure lLoc points to world location 239 | if(lLoc != worldLoc) { 240 | fs.unlink(path.join(bin, world), function(err) { 241 | if(err && err.code != 'ENOENT') { 242 | if(cb) cb(err); 243 | return; 244 | } 245 | 246 | self.log.debug('Creating symlink for %s at %s', world, worldLoc); 247 | fs.symlink(worldLoc, path.join(bin, world), function(err) { 248 | if(err) { if(cb) cb(err); return; } 249 | 250 | self.log.debug('Created symlink for %s at %s', world, worldLoc); 251 | if(cb) cb(null); 252 | }); 253 | }); 254 | } else { 255 | self.log.debug('Link location already points to where it should'); 256 | if(cb) cb(null); 257 | } 258 | }); 259 | }; 260 | 261 | WorldManager.prototype._buildBackupCommand = function(world) { 262 | var to = self.worker.files._datePath(path.join(self.backup.maps.path, world + '_'), '.tar.gz2'), 263 | from = path.join(self.paths.worldDisk, world); 264 | 265 | return (function(to, from) { 266 | return function(next) { 267 | self.log.info('Backing up ' + world + ' to ' + dname + '...'); 268 | cp.exec('tar -hcjf ' + to + ' ' + from, function(err, stdout, stderr) { 269 | if(err !== null) { 270 | self.log.error(err, 'Error taring backup for %s', world); 271 | } 272 | 273 | next(err, { stdout: stdout, stderr: stderr }); 274 | }); 275 | }; 276 | })(to, from); 277 | }; 278 | 279 | WorldManager.prototype._defineGetters = function() { 280 | var self = this; 281 | 282 | //self.__defineGetter__('something', function() {}); 283 | }; 284 | 285 | WorldManager.prototype._defineSetters = function() { 286 | var self = this; 287 | 288 | //self.__defineSetter__('something', function(val) {}); 289 | }; 290 | -------------------------------------------------------------------------------- /lib/psm/cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cli.js: provides the CLI used to interact with the PSM application 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENCE 6 | * 7 | **/ 8 | 9 | ////////// 10 | // Required Includes 11 | /////////////////////////// 12 | var fs = require('fs'), 13 | path = require('path'), 14 | cp = require('child_process'), 15 | util = require('util'), 16 | eyes = require('eyes'), 17 | psm = require('../psm'), 18 | flatiron = require('flatiron'); 19 | 20 | var cli = exports, 21 | inspect = eyes.inspector({ 22 | stream: null 23 | }); 24 | 25 | ////////// 26 | // Setup flatiron CLI 27 | /////////////////////////// 28 | var app = flatiron.app, 29 | actions = [ 30 | 'start', 31 | 'stop', 32 | 'restart', 33 | 'status', 34 | 'import', 35 | 'list', 36 | 'config', 37 | 'set' 38 | ], 39 | argvOpts = cli.argvOpts = { 40 | help: { alias: 'h' }, 41 | silent: { alias: 's', boolean: true }, 42 | verbose: { alias: 'v', boolean: true }, 43 | debug: { alias: 'd', boolean: true }, 44 | version: { boolean: true } 45 | }, 46 | help = [ 47 | 'Usage: psm ACTION [options]', 48 | '', 49 | 'Manages the specified Minecraft server from a central API', 50 | '', 51 | 'Actions:', 52 | ' start Starts ', 53 | ' stop Stops ', 54 | ' restart Restarts ', 55 | ' status Shows the status of ', 56 | ' import Imports json server configuration to manage', 57 | ' list Lists all managed servers', 58 | ' config [key] Displays the psm configuration', 59 | ' set Sets the configuration to ', 60 | '', 61 | 'Options:', 62 | ' -h, --help Displays this help message', 63 | ' -s, --silent Silences all output from the cli', 64 | ' -v, --verbose Enables verbose output from the cli', 65 | ' -d, --debug Enables debug output from the cli', 66 | ' --version Displays the version of the psm module', 67 | '', 68 | 'Examples:', 69 | ' restart minecraft server named "minecraft":', 70 | ' > psm restart minecraft', 71 | '', 72 | ' import a json config for a new server to manage:', 73 | ' > psm import server.json', 74 | '', 75 | ' import a json config for the psm module:', 76 | ' > psm config config.json', 77 | '' 78 | ]; 79 | 80 | flatiron.app.use(flatiron.plugins.cli, { 81 | argv: argvOpts, 82 | usage: help 83 | }); 84 | 85 | ////////// 86 | // Command handlers 87 | /////////////////////////// 88 | app.cmd(/start (.+)/, cli.startServer = function(server) { 89 | //var server = app.argv._[1]; 90 | 91 | //psm.log.silly('Got start command for server: %s', server); 92 | 93 | psm.log.info('Starting server %s', server); 94 | psm.manager.cmd('start', server, function(err, res) { 95 | if(err) { 96 | psm.log.error(err, 'Unable to start server.', function() { 97 | process.exit(1); 98 | }); 99 | } else { 100 | psm.log.info('Server has started.', function() { 101 | process.exit(0); 102 | }); 103 | } 104 | }); 105 | }); 106 | 107 | app.cmd(/stop (.+)/, cli.stopServer = function(server) { 108 | //var server = app.argv._[1]; 109 | 110 | //psm.log.silly('Got stop command for server: %s', server); 111 | 112 | psm.log.info('Stopping server %s', server); 113 | psm.manager.cmd('stop', server, function(err, res) { 114 | if(err) { 115 | psm.log.error(err, 'Unable to stop server.', function() { 116 | process.exit(1); 117 | }); 118 | } else { 119 | psm.log.info('Server has stopped.', function() { 120 | process.exit(0); 121 | }); 122 | } 123 | }); 124 | }); 125 | 126 | app.cmd(/restart (.+)/, cli.restartServer = function(server) { 127 | //var server = app.argv._[1]; 128 | 129 | //psm.log.silly('Got restart command for server: %s', server); 130 | 131 | psm.log.info('Restarting server %s', server); 132 | psm.manager.cmd('restart', server, function(err, res) { 133 | if(err) { 134 | psm.log.error(err, 'Unable to restart server.', function() { 135 | process.exit(1); 136 | }); 137 | } else { 138 | psm.log.info('Server has restarted.', function() { 139 | process.exit(0); 140 | }); 141 | } 142 | }); 143 | }); 144 | 145 | app.cmd(/status (.+)/, cli.showServerInfo = function(server) { 146 | //var server = app.argv._[1]; 147 | 148 | //psm.log.silly('Got status command for server: %s', server); 149 | 150 | psm.log.info('Getting status of server %s', server); 151 | psm.manager.cmd('status', server, function(err, res) { 152 | //TODO: Parse info object 153 | if(err) { 154 | psm.log.error(err, 'Unable to get server status.', function() { 155 | process.exit(1); 156 | }); 157 | } else { 158 | psm.log.info(res, function() { 159 | process.exit(0); 160 | }); 161 | } 162 | }); 163 | }); 164 | 165 | app.cmd('list', cli.list = function() { 166 | psm.log.silly('Got list command.'); 167 | 168 | psm.manager.cmd('list', function(err, res) { 169 | //TODO: Parse servers 170 | if(err) { 171 | psm.log.error(err, 'Unable to get server list.', function() { 172 | process.exit(1); 173 | }); 174 | } else { 175 | psm.log.info(res, function() { 176 | process.exit(0); 177 | }); 178 | } 179 | }); 180 | }); 181 | 182 | app.cmd(/(add|rm) server (.+)?/, cli.importServers = function(cmd, file) { 183 | //var cmd = app.argv._[0], 184 | //file = app.argv._[2] || null; 185 | 186 | psm.log.silly('Got %s command%s', cmd, (file ? ', with arg: ' + file : '.')); 187 | 188 | //open file ad get server object 189 | if(cmd == 'add') { 190 | if(file) { 191 | try { 192 | file = require(path.resolve(file)); 193 | } catch(e) { 194 | psm.log.error(e, 'Unable to open file: %s', file, function() { 195 | process.exit(1); 196 | }); 197 | return; 198 | } 199 | 200 | psm.log.debug('Requesting server add.', file); 201 | psm.manager.cmd(cmd, 'server', file, function(err, res) { 202 | psm.log.silly({ err: err, res: res }, 'Add response'); 203 | if(err) { 204 | psm.log.error(err, 'Unable to add server', function() { 205 | process.exit(1); 206 | }); 207 | } else { 208 | psm.log.info('New server added to manager.', function() { 209 | process.exit(0); 210 | }); 211 | } 212 | }); 213 | } else { 214 | //TODO: Prompts 215 | var serv = {}; 216 | psm.manager.cmd(cmd, 'server', file, function(err, res) { 217 | psm.log.silly({ err: err, res: res }, 'Add Response'); 218 | if(err) { 219 | psm.log.error(err, 'Unable to add server', function() { 220 | process.exit(1); 221 | }); 222 | } else { 223 | psm.log.info('New server added to manager.', function() { 224 | process.exit(0); 225 | }); 226 | } 227 | }); 228 | } 229 | } else { 230 | psm.log.debug('Reqesting server removal.', file); 231 | psm.manager.cmd(cmd, 'server', file, function(err, res) { 232 | psm.log.silly({ err: err, res: res }, 'Remove Response'); 233 | if(err) { 234 | psm.log.error(err, 'Unable to remove server', function() { 235 | process.exit(1); 236 | }); 237 | } else { 238 | psm.log.info('Server removed from manager.', function() { 239 | process.exit(0); 240 | }); 241 | } 242 | }); 243 | } 244 | }); 245 | 246 | app.cmd(/config ([\w-_]+)?/, cli.config = function(key) { 247 | //var key = app.argv._[1]; 248 | 249 | psm.log.silly('Got config command, with key: %s', key); 250 | 251 | psm.log.info('Current Configuration:\n%s', inspect(key ? psm.config[key] : psm.config).cyan, function() { 252 | process.exit(0); 253 | }); 254 | }); 255 | 256 | app.cmd(/set ([\w-_]+) (.+)/, cli.set = function(key, val) { 257 | //var key = app.argv._[1], 258 | //val = app.argv._[2]; 259 | 260 | psm.log.silly('Got set command, with key: %s, value: %s', key, val); 261 | 262 | psm.manager.cmd('config', key, val, function(err, res) { 263 | psm.log.silly({ err: err, res: res }, 'Set config response'); 264 | if(err) { 265 | psm.log.error(err, 'Unable to set configuration', function() { 266 | process.exit(1); 267 | }); 268 | } else { 269 | psm.log.info('Configuration set!', function() { 270 | process.exit(0); 271 | }); 272 | } 273 | }); 274 | }); 275 | 276 | cli.start = function() { 277 | app.init(function() { 278 | //psm shows status 279 | if(app.argv._.length && actions.indexOf(app.argv._[0]) === -1) { 280 | app.argv._.push(app.argv._[0]); 281 | return cli.showServerInfo(); 282 | } 283 | 284 | //-h, --help shows help 285 | if(app.argv.help) { 286 | var done = 0; 287 | 288 | help.forEach(function(line) { 289 | psm.log.info(line, function() { 290 | done++; 291 | if(done == help.length) 292 | process.exit(0); 293 | }); 294 | }); 295 | return; 296 | } 297 | 298 | //--version to show version 299 | if(app.argv.version) { 300 | psm.log.info('Version: %s', psm.version, function() { 301 | process.exit(0); 302 | }); 303 | return; 304 | } 305 | 306 | //-s, --silent to be silent 307 | if(app.argv.silent) { 308 | psm.log.transports[1].level = 'silent'; 309 | } 310 | 311 | //-v, --verbose to be verbose 312 | if(app.argv.verbose) { 313 | psm.log.transports[1].level = 'verbose'; 314 | } 315 | 316 | //-d, --debug to be debug 317 | if(app.argv.debug) { 318 | psm.log.transports[1].level = 'debug'; 319 | } 320 | 321 | app.start(); 322 | }); 323 | }; --------------------------------------------------------------------------------