diff --git a/doomsday/host/doomsday-host b/doomsday/host/doomsday-host index c81c8fbfaa..457d5d4a79 100755 --- a/doomsday/host/doomsday-host +++ b/doomsday/host/doomsday-host @@ -7,15 +7,35 @@ # - log file rotation # - automatic updates by rebuilding from source with custom options +import sys import os +import time +import string +import pickle +import subprocess +import signal from xml.dom.minidom import parse -commonOptions = '' +def homeFile(fn): + return os.path.join(os.getenv('HOME'), fn) + +commonOptions = [] rebuildTimes = [] branch = 'master' -mainLogFile = os.path.join(os.getenv('HOME'), 'doomsdayhost.log') +mainLogFileName = homeFile('doomsdayhost.log') +logFile = None servers = [] +pidFileName = homeFile('.doomsdayhost.pid') +buildDir = '' +qmakeCommand = '' + +def msg(text): + if not logFile: + print text + else: + print >> logFile, time.asctime() + ': ' + text + def getText(nodelist): rc = [] @@ -26,66 +46,248 @@ def getText(nodelist): def getContent(node): - return getText(node.childNodes) - + return str(getText(node.childNodes)) + def parseRebuildTimes(rebuilds): - times = [] - for node in rebuilds.getElementsByTagName('rebuild'): - times.append((str(node.getAttribute('weekday')), - int(node.getAttribute('hour')), - int(node.getAttribute('minute')))) - return times + times = [] + for node in rebuilds.getElementsByTagName('rebuild'): + times.append((str(node.getAttribute('weekday')), + int(node.getAttribute('hour')), + int(node.getAttribute('minute')))) + return times class Server: - def __init__(self): - self.port = 13209 - self.game = None - self.name = 'Multiplayer Server' - self.info = '' - self.runtime = '' - self.options = '' - - + def __init__(self): + self.port = 13209 + self.game = None + self.name = 'Multiplayer Server' + self.info = '' + self.runtime = '' + self.options = [] + + def parseServers(nodes): - svs = [] - for node in nodes: - s = Server() - s.port = int(node.getAttribute('port')) - s.game = str(node.getAttribute('game')) - s.name = str(node.getAttribute('name')) - s.info = str(node.getAttribute('info')) - s.runtime = str(node.getAttribute('dir')) - s.options = str(node.getAttribute('options')) - svs.append(s) - return svs - + svs = [] + for node in nodes: + s = Server() + s.port = int(node.getAttribute('port')) + s.game = str(node.getAttribute('game')) + s.name = str(node.getAttribute('name')) + s.info = str(node.getAttribute('info')) + s.runtime = str(node.getAttribute('dir')) + for opt in node.getElementsByTagName('option'): + s.options.append(getContent(opt)) + svs.append(s) + return svs + def parseConfig(fn): - global commonOptions - global rebuildTimes - global mainLogFile - global branch - global servers - - cfg = parse(fn) - commonOptions = getContent(cfg.getElementsByTagName('options')[0]) - - rebuildTimes = \ - parseRebuildTimes(cfg.getElementsByTagName('rebuildTimes')[0]) - - mainLogFile = getContent(cfg.getElementsByTagName('logFile')[0]) - branch = getContent(cfg.getElementsByTagName('branch')[0]) - - servers = \ - parseServers(cfg.getElementsByTagName('servers')[0]. - getElementsByTagName('server')) - + global commonOptions + global rebuildTimes + global mainLogFileName + global branch + global servers + global buildDir + global qmakeCommand + + cfg = parse(fn) + for opt in cfg.getElementsByTagName('option'): + if opt.parentNode.tagName == u'hostconfig': + commonOptions.append(getContent(opt)) + + rebuildTimes = \ + parseRebuildTimes(cfg.getElementsByTagName('rebuildTimes')[0]) + + mainLogFileName = getContent(cfg.getElementsByTagName('logFile')[0]) + branch = getContent(cfg.getElementsByTagName('branch')[0]) + buildDir = getContent(cfg.getElementsByTagName('buildDir')[0]) + qmakeCommand = getContent(cfg.getElementsByTagName('qmakeCommand')[0]) + + servers = \ + parseServers(cfg.getElementsByTagName('servers')[0]. + getElementsByTagName('server')) + + +def isStale(fn): + """Files are considered stale after some time has passed.""" + age = time.time() - os.stat(fn).st_ctime + if age > 2*60*60: + msg(fn + ' is stale, ignoring it.') + return True + return False + +def timeInRange(mark, start, end): + def breakTime(t): + gt = time.gmtime(t) + return (time.strftime('%a', gt), + time.strftime('%H', gt), + time.strftime('%M', gt)) + def mins(h, m): return m + 60 * h + + s = breakTime(start) + e = breakTime(end) + if mark[0] != s[0] or mark[0] != e[0]: return False + markMins = mins(mark[1], mark[2]) + if markMins < mins(s[1], s[2]) or markMins >= mins(e[1], e[2]): + return False + return True + + +def checkPid(pid): + try: + os.kill(pid, 0) + return True + except OSError: + return False + + +class State: + def __init__(self): + self.lastRun = self.now() + self.pids = {} # port => process id + + def now(self): + return int(time.time()) + + def updateTime(self): + self.lastRun = self.now() + + def isTimeForRebuild(self): + for rebuild in rebuildTimes: + if timeInRange(rebuild, self.lastRun, self.now()): + return True + return False + + def killAll(self): + for port in self.pids: + pid = self.pids[port] + msg('Stopping server at port %i (pid %i)...' % (port, pid)) + try: + os.kill(pid, signal.SIGTERM) + except OSError: + pass + self.pids = {} + + def isRunning(self, sv): + if sv.port not in self.pids: return False + pid = self.pids[sv.port] + # Is this process still around? + return checkPid(pid) + + def start(self, sv): + if self.isRunning(sv): + msg('Server %i already running according to state!' % (sv.port)) + return + + args = ['/usr/bin/doomsday'] + + cfgFn = homeFile('server%i.cfg' % sv.port) + cfgFile = file(cfgFn, 'wt') + print >> cfgFile, 'server-name "%s"' % sv.name + print >> cfgFile, 'server-info "%s"' % sv.info + print >> cfgFile, 'net-ip-port %i' % sv.port + cfgFile.close() + + args += ['-game', sv.game] + args += ['-parse', cfgFn] + args += sv.options + commonOptions + + try: + po = subprocess.Popen(args) + pid = po.pid + time.sleep(1) + if po.poll() is not None: + raise OSError('terminated') + self.pids[sv.port] = pid + msg('Started server at port %i (pid %i).' % (sv.port, pid)) + except OSError, x: + msg('Failed to start server at port %i: %s' % (sv.port, str(x))) + + +def run(cmd, mustSucceed=True): + result = subprocess.call(cmd, shell=True) + if result and mustSucceed: + raise Exception("Failed: " + cmd) + + +def rebuildAndInstall(): + msg('Rebuilding from branch %s.' % branch) + try: + os.chdir(buildDir) + run('git checkout ' + branch) + run('git pull') + run('sudo_make uninstall', mustSucceed=False) + run('make clean') + run(qmakeCommand) + run('make') + run('sudo_make install') + except Exception: + msg('Failed to build!') + return False + + msg('Successful rebuild from branch %s.' % branch) + return True + + +def startInstance(): + # Check for an existing pid file. + pid = homeFile(pidFileName) + if os.path.exists(pid): + if not isStale(pid): + # Cannot start right now -- will be retried later. + sys.exit(0) + print >> file(pid, 'w'), str(os.getpid()) + + global logFile + logFile = file(mainLogFileName, 'at') + + +def endInstance(): + global logFile + logFile.close() + logFile = None + try: + os.remove(homeFile(pidFileName)) + except Exception: + pass + + def main(): - cfg = parseConfig(os.path.join(os.getenv('HOME'), '.doomsdayhostrc')) + startInstance() + + cfg = parseConfig(homeFile('.doomsdayhostrc')) + + # Is there a saved state? + stateFn = homeFile('.doomsdayhoststate.bin') + if os.path.exists(stateFn): + state = pickle.load(file(stateFn, 'rb')) + else: + state = State() + + # What should we do? + # Is it time to rebuild from source? + if state.isTimeForRebuild(): + state.killAll() + if rebuildAndInstall(): + # Success! Update the timestamp. + state.updateTime() + else: + state.updateTime() + + # Are all the servers up and running? + for sv in servers: + if not state.isRunning(sv): + state.start(sv) + + # Save the state. + pickle.dump(state, file(stateFn, 'wb')) + + # We're done. + endInstance() if __name__ == '__main__': - main() + main() diff --git a/doomsday/host/doomsdayhostrc-example b/doomsday/host/doomsdayhostrc-example index 5b65027a0e..d2d1c42269 100644 --- a/doomsday/host/doomsdayhostrc-example +++ b/doomsday/host/doomsdayhostrc-example @@ -1,7 +1,12 @@ host.log - -dedicated -server -parse /home/jaakko/host.cfg + + + + master + /Users/jaakko/src/deng/doomsday-build + qmake -r ../doomsday/doomsday.pro CONFIG+='debug deng_packres' @@ -11,42 +16,58 @@ + dir="/Users/jaakko/runtime/doom"> + + + + dir="/Users/jaakko/runtime/doom2"> + + + + dir="/Users/jaakko/runtime/heretic"> + + + + dir="/Users/jaakko/runtime/hexen"> + + + + dir="/Users/jaakko/runtime/doom-dm"> + + + + dir="/Users/jaakko/runtime/doom2-dm"> + + + + dir="/Users/jaakko/runtime/heretic-dm"> + + + + dir="/Users/jaakko/runtime/hexen-dm"> + + + \ No newline at end of file