Navigation Menu

Skip to content

Commit

Permalink
Host: Functionality fully implemented
Browse files Browse the repository at this point in the history
But not tested yet.
  • Loading branch information
skyjake committed Jan 31, 2012
1 parent 1a79cf6 commit 3f61aae
Show file tree
Hide file tree
Showing 2 changed files with 292 additions and 69 deletions.
306 changes: 254 additions & 52 deletions doomsday/host/doomsday-host
Expand Up @@ -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 = []
Expand All @@ -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()

0 comments on commit 3f61aae

Please sign in to comment.