123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- # 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/<pid>/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()
|