# run doom process on a series of maps # can be used for regression testing, or to fetch media # keeps a log of each run ( see getLogfile ) # currently uses a basic stdout activity timeout to decide when to move on # using a periodic check of /proc//status SleepAVG # when the sleep average is reaching 0, issue a 'quit' to stdout # keeps serialized run status in runner.pickle # NOTE: can be used to initiate runs on failed maps only for instance etc. # TODO: use the serialized and not the logs to sort the run order # TODO: better logging. Use idLogger? # TODO: configurable event when the process is found interactive # instead of emitting a quit, perform some warning action? import sys, os, commands, string, time, traceback, pickle from twisted.application import internet, service from twisted.internet import protocol, reactor, utils, defer from twisted.internet.task import LoopingCall class doomClientProtocol( protocol.ProcessProtocol ): # ProcessProtocol API def connectionMade( self ): self.logfile.write( 'connectionMade\n' ) def outReceived( self, data ): print data self.logfile.write( data ) def errReceived( self, data ): print 'stderr: ' + data self.logfile.write( 'stderr: ' + data ) def inConnectionLost( self ): self.logfile.write( 'inConnectionLost\n' ) def outConnectionLost( self ): self.logfile.write( 'outConnectionLost\n' ) def errConnectionLost( self ): self.logfile.write( 'errConnectionLost\n' ) def processEnded( self, status_object ): self.logfile.write( 'processEnded %s\n' % repr( status_object ) ) self.logfile.write( time.strftime( '%H:%M:%S', time.localtime( time.time() ) ) + '\n' ) self.logfile.close() self.deferred.callback( None ) # mac management def __init__( self, logfilename, deferred ): self.logfilename = logfilename self.logfile = open( logfilename, 'a' ) self.logfile.write( time.strftime( '%H:%M:%S', time.localtime( time.time() ) ) + '\n' ) self.deferred = deferred class doomService( service.Service ): # current monitoring state # 0: nothing running # 1: we have a process running, we're monitoring it's CPU usage # 2: we issued a 'quit' to the process's stdin # either going to get a processEnded, or a timeout # 3: we forced a kill because of error, timeout etc. state = 0 # load check period check_period = 10 # pickled status file pickle_file = 'runner.pickle' # stores status indexed by filename # { 'mapname' : ( state, last_update ), .. } status = {} # start the maps as multiplayer server multiplayer = 0 def __init__( self, bin, cmdline, maps, sort = 0, multiplayer = 0, blank_run = 0 ): self.p_transport = None self.multiplayer = multiplayer self.blank_run = blank_run if ( self.multiplayer ): print 'Operate in multiplayer mode' self.bin = os.path.abspath( bin ) if ( type( cmdline ) is type( '' ) ): self.cmdline = string.split( cmdline, ' ' ) else: self.cmdline = cmdline self.maps = maps if ( os.path.exists( self.pickle_file ) ): print 'Loading pickled status %s' % self.pickle_file handle = open( self.pickle_file, 'r' ) self.status = pickle.load( handle ) handle.close() if ( sort ): print 'Sorting maps oldest runs first' maps_sorted = [ ] for i in self.maps: i_log = self.getLogfile( i ) if ( os.path.exists( i_log ) ): maps_sorted.append( ( i, os.path.getmtime( i_log ) ) ) else: maps_sorted.append( ( i, 0 ) ) maps_sorted.sort( lambda x,y : cmp( x[1], y[1] ) ) self.maps = [ ] if ( blank_run ): self.maps.append( 'blankrun' ) for i in maps_sorted: self.maps.append( i[ 0 ] ) print 'Sorted as: %s\n' % repr( self.maps ) def getLogfile( self, name ): return 'logs/' + string.translate( name, string.maketrans( '/', '-' ) ) + '.log' # deferred call when child process dies def processEnded( self, val ): print 'child has died - state %d' % self.state self.status[ self.maps[ self.i_map ] ] = ( self.state, time.time() ) self.i_map += 1 if ( self.i_map >= len( self.maps ) ): reactor.stop() else: self.nextMap() def processTimeout( self ): self.p_transport.signalProcess( "KILL" ) def sleepAVGReply( self, val ): try: s = val[10:][:-2] print 'sleepAVGReply %s%%' % s if ( s == '0' ): # need twice in a row if ( self.state == 2 ): print 'child process is interactive' self.p_transport.write( 'quit\n' ) else: self.state = 2 else: self.state = 1 # else: # reactor.callLater( self.check_period, self.checkCPU ) except: print traceback.format_tb( sys.exc_info()[2] ) print sys.exc_info()[0] print 'exception raised in sleepAVGReply - killing process' self.state = 3 self.p_transport.signalProcess( 'KILL' ) def sleepAVGTimeout( self ): print 'sleepAVGTimeout - killing process' self.state = 3 self.p_transport.signalProcess( 'KILL' ) # called at regular intervals to monitor the sleep average of the child process # when sleep reaches 0, it means the map is loaded and interactive def checkCPU( self ): if ( self.state == 0 or self.p_transport is None or self.p_transport.pid is None ): print 'checkCPU: no child process atm' return defer = utils.getProcessOutput( '/bin/bash', [ '-c', 'cat /proc/%d/status | grep SleepAVG' % self.p_transport.pid ] ) defer.addCallback( self.sleepAVGReply ) defer.setTimeout( 2, self.sleepAVGTimeout ) def nextMap( self ): self.state = 0 name = self.maps[ self.i_map ] print 'Starting map: ' + name logfile = self.getLogfile( name ) print 'Logging to: ' + logfile if ( self.multiplayer ): cmdline = [ self.bin ] + self.cmdline + [ '+set', 'si_map', name ] if ( name != 'blankrun' ): cmdline.append( '+spawnServer' ) else: cmdline = [ self.bin ] + self.cmdline if ( name != 'blankrun' ): cmdline += [ '+devmap', name ] print 'Command line: ' + repr( cmdline ) self.deferred = defer.Deferred() self.deferred.addCallback( self.processEnded ) self.p_transport = reactor.spawnProcess( doomClientProtocol( logfile, self.deferred ), self.bin, cmdline , path = os.path.dirname( self.bin ), env = os.environ ) self.state = 1 # # setup the CPU usage loop # reactor.callLater( self.check_period, self.checkCPU ) def startService( self ): print 'doomService startService' loop = LoopingCall( self.checkCPU ) loop.start( self.check_period ) self.i_map = 0 self.nextMap() def stopService( self ): print 'doomService stopService' if ( not self.p_transport.pid is None ): self.p_transport.signalProcess( 'KILL' ) # serialize print 'saving status to %s' % self.pickle_file handle = open( self.pickle_file, 'w+' ) pickle.dump( self.status, handle ) handle.close()