import time import tempfile import random import traceback import thread import trac.core # for interface and extension points from twisted.spread import pb from twisted.internet import reactor import config from interfaces import IAssignment import assignments # exceptions class BattleError(pb.Error): """used to communicate problems from the server to the client""" class RemoteException(Exception): """raised on the client to wrap a remote exception""" # report related stuff class Error(object): def __init__(self, result, exception, e, tb): self.result = result self.exception = exception self.message = e self.traceback = traceback.format_tb(tb) class Parted(object): def __init__(self): self.result = None class Invalid(object): def __init__(self, result, message): self.result = result self.message = message class Success(object): def __init__(self, result, code_size, time_spent, execution_time): self.result = result self.code_size = code_size self.time_spent = time_spent self.execution_time = execution_time class Report(pb.Referenceable): """a report""" def __init__(self): self.results = {} def report_part(self, username): self.results[username] = Parted() def report_error(self, username, result, exception, e, tb): self.results[username] = Error(result, exception, e, tb) def report_invalid(self, username, result, error): self.results[username] = Invalid(result, error) def report_success(self, username, result, code_size, time_spent, execution_time): self.results[username] = Success(result, code_size, time_spent, execution_time) def __str__(self): ret = ['results:'] for username, result in self.results.iteritems(): ret.append('user %s' % (username,)) ret.append('') ret.append('submitted code:') ret.append(str(result.result)) ret.append('') if isinstance(result, Error): ret.append('Error: %s - %s' % (result.exception, result.message)) ret.append('') ret.append('\n'.join(result.traceback)) elif isinstance(result, Invalid): ret.append('Invalid: %s' % (result.message)) elif isinstance(result, Success): ret.append('Success...') ret.append('Code size: %s instructions' % (result.code_size,)) ret.append('Time spent: %s seconds' % (result.time_spent,)) ret.append('Execution time of loops: %s' % (result.execution_time,)) ret.append('') return '\n'.join(ret) def remote_stringify(self): return str(self) # server related stuff class States: INITIALIZING = -1 ACCEPTING = 0 PLAYING = 1 REPORTING = 2 def log(m, severity=0): """log a message m is the message, severity can be 0 (informative), 1 (warning) or 2 (error) """ print 'log %s: %s' % (time.strftime('%Y/%m/%d %H:%M:%S'), m) class PyBattleServer(trac.core.Component, pb.Root): """simple game server""" # automagically find all IAssignment classes and make a 'list' of # instances available assignments = trac.core.ExtensionPoint(IAssignment) status = States.INITIALIZING def __init__(self): self.clients = {} self._initialize() log('server initialized') # remote API def remote_connect(self, username, client): """try to register for the next game""" if username in self.clients: raise BattleError('name already in use') self.clients[username] = client client.server_data = {'result': None, 'joined': False, 'parted': False, } log('user %s connected' % (username,)) self.chat('pybattle', 'user %s connected' % (username,)) self.chat('pybattle', 'current users now: %s' % (', '.join(self.clients.keys()))) def remote_join(self, username): if self.status != States.ACCEPTING: raise BattleError('joining currently not allowed') if username not in self.clients: raise BattleError('unkown username') data = self.clients[username].server_data if data['parted']: raise BattleError('already parted') if data['joined']: raise BattleError('already joined') data['joined'] = True log('user %s joined the game' % (username,)) self.chat('pybattle', 'user %s joined' % (username,)) def remote_start(self, *args): """start this round""" if not self.status == States.ACCEPTING: raise BattleError('not ready to start now...') log('starting game') players = sorted( [u for u, c in self.clients.iteritems() if c.server_data['joined'] and not c.server_data['parted'] ]) self.chat('pybattle', 'starting game with players: %s' % (', '.join(players))) self.status = States.PLAYING # choose a random assignment and self.assignment = assignment = random.choice(list(self.assignments)) self.assignment.start_time = time.time() question = assignment.question log('assignment: %s' % (question,)) for username, client in self.clients.iteritems(): if client.server_data['parted']: continue if not client.server_data['joined']: client.callRemote('print_question', question) continue log('calling start_assignment for user %s' % (username,)) client.callRemote('start_assignment', question) def remote_stop(self, username, result): """save result for user""" if not username in self.clients: print 'stop' raise BattleError('unknown user %s' % (username,)) data = self.clients[username].server_data if data['parted'] or not data['joined']: raise BattleError('not joined') log('results for user %s: %s' % (username, result)) self.chat('pybattle', 'results for user %s are in' % (username,)) data['result'] = result data['time_spent'] = time.time() - self.assignment.start_time # see if all results are in now, if so procss and present them for username, client in self.clients.iteritems(): if not client.server_data['joined']: continue if (not client.server_data['parted'] and client.server_data['result'] is None): return self._process_and_present() def remote_part(self, username): if not username in self.clients: print 'part' raise BattleError('unknown user %s' % (username,)) data = self.clients[username].server_data if data['parted'] or not data['joined']: raise BattleError('not joined') data['joined'] = False data['parted'] = True log('%s parted' % (username,)) self.chat('pybattle', '%s parted' % (username,)) # if there are no clients left, stop the game and restart for username, client in self.clients.iteritems(): print username, repr(client.server_data) if (not client.server_data['parted'] and client.server_data['result'] is None): return self._process_and_present() def remote_disconnect(self, username): if not username in self.clients: print 'disconnect' raise BattleError('unknown user %s' % (username,)) del self.clients[username] print 'disconnected' self.chat('pybattle', '%s disconnected' % (username,)) if self.status == States.PLAYING: for username, client in self.clients.iteritems(): if (not client.server_data['parted'] and client.server_data_result is None): return self._process_and_present() def remote_chat(self, username, message): if (not username in self.clients or self.clients[username].server_data['parted']): raise BattleError('no such user %s' % (username,)) self.chat(username, message) # public/own API def chat(self, user, message): log('chat: %s said %s' % (user, message)) for username, client in self.clients.iteritems(): d = client.callRemote('chat', user, message) # private stuff def _initialize(self): log('initializing') if self.status != States.INITIALIZING: raise Exception, 'can not initialize now' for username, client in self.clients.iteritems(): client.server_data = { 'result': None, 'joined': False, 'parted': False, } self.status = States.ACCEPTING log('accepting') def _cleanup(self): log('cleanup') self.status = States.INITIALIZING def _process_and_present(self): """process the results and present the report to clients""" self.status = States.REPORTING report = self._process_results() for username, client in self.clients.iteritems(): if client.server_data['parted']: continue log('presenting report to %s' % (username,)) client.callRemote('present_report', report) # we're done, can start accepting new registrations again self._cleanup() self._initialize() def _process_results(self): """create a Report instance, fill it with data about the results""" report = Report() for username, client in self.clients.iteritems(): result = client.server_data['result'] self.assignment.test(username, client, result, report) return report # the client class PyBattleClient(pb.Root): """game client interface""" connected = False joined = False playing = False result = None def __init__(self, username): self.username = username # PUBLIC API def _init(self, server): self.server = server def connect(self): """connect to the server""" if self.connected: raise BattleError('already connected') d = self.server.callRemote('connect', self.username, self) d.addCallbacks(self._connected, self._error) def join(self): """join the (if any) upcoming game""" if not self.connected: raise BattleError('not connected yet') if self.joined: raise BattleError('already joined') d = self.server.callRemote('join', self.username) d.addCallbacks(self._joined, self._error) def start(self): """start the game *for all clients!*""" if not self.connected: raise BattleError('not connected yet!') if not self.joined: raise BattleError( 'you have to join the game in order to start it') if self.playing: raise BattleError('already playing!') # just to be sure ;) self.result = None d = self.server.callRemote('start', self) d.addErrback(self._error) def stop(self): """stop and save the results""" if not self.connected: raise BattleError('not connected yet') if not self.joined: raise BattleError('not joined yet') if not self.playing: raise BattleError('not playing yet') d = self.server.callRemote('stop', self.username, self.result) d.addErrback(self._error) self._reset() def part(self): """stop and discard the results (forfeit)""" if not self.connected or not self.joined: raise BattleError('not joined yet!') if not self.playing: raise BattleError('not playing yet!') d = self.server.callRemote('part', self.username) d.addErrback(self._error) self._reset() def disconnect(self): """disconnect from the server""" if not self.connected: raise BattleError('not connected!') self._reset() d = self.server.callRemote('disconnect', self.username) d.addCallbacks(self._quit, self._error) self.connected = False def set_result(self, result): self.result = result def chat(self, message): self.server.callRemote('chat', self.username, message) def show_info(self, info): """show info message""" self._print(info) # REMOTE API (called by server) def remote_print_question(self, question): """print the question, for when not joined""" self.show_info('assignment: %s' % question) def remote_start_assignment(self, question): """start editing""" self.show_info('assignment: %s' % (question,)) self.result = None self.playing = True def remote_present_report(self, report): """present the report""" # XXX a bit strange to have this here... seems to be correct though? self.joined = False def print_report(report): self._print(report) report.callRemote('stringify').addCallbacks(print_report, self._error) def remote_chat(self, username, message): """present chat message""" self._print('%s said: %s' % (username, message)) def _connected(self, ret=None): self.connected = True def _joined(self, ret=None): self.joined = True def _quit(self, ret=None): if self.connected: self.disconnect() else: reactor.stop() def _print(self, s): print s def _error(self, err): if type(err) not in [str, unicode]: if not isinstance(err, pb.Error): raise RemoteException(err) err = err.getErrorMessage() self._print('err: %s' % err) def _reset(self): self.playing = False self.joined = False self.result = False # functions to start the server and (a quite useless implementation of the) # client def start_server(): warning = ['You are about to run a *very* insecure program!', '', 'This server executes arbitrary Python code presented by ', 'arbitrary clients in an unrestricted manner. This makes it', 'a very insecure system, that can easily be used for', 'malicious purposes. Make *very* sure the user running the', 'server doesn\'t have any privileges whatsoever on the', 'system the server runs on, preferrably behind a firewall,', 'and play only with people you trust will not do anything', 'unsportsmenlike (unless your rules are to destroy the server', 'as efficiently as possible, or something ;).', '', 'Enter \'I am sure, start the server!\' to continue, any', 'other input to quit.', ''] if raw_input('\n'.join(warning)) != 'I am sure, start the server!': return component_manager = trac.core.ComponentManager() root = PyBattleServer(component_manager) log('starting server with assignments %s' % ([a.__class__.__name__ for a in root.assignments],)) reactor.listenTCP(config.server_port, pb.PBServerFactory(root)) reactor.run() def start_client(username): client = PyBattleClient(username) factory = pb.PBClientFactory() reactor.connectTCP(config.server_hostname, config.server_port, factory) d = factory.getRootObject() d.addCallbacks(client._init, client._error) reactor.run()