1 # run doom process on a series of maps
2 # can be used for regression testing, or to fetch media
3 # keeps a log of each run ( see getLogfile )
5 # currently uses a basic stdout activity timeout to decide when to move on
6 # using a periodic check of /proc/<pid>/status SleepAVG
7 # when the sleep average is reaching 0, issue a 'quit' to stdout
9 # keeps serialized run status in runner.pickle
10 # NOTE: can be used to initiate runs on failed maps only for instance etc.
12 # TODO: use the serialized and not the logs to sort the run order
14 # TODO: better logging. Use idLogger?
16 # TODO: configurable event when the process is found interactive
17 # instead of emitting a quit, perform some warning action?
19 import sys, os, commands, string, time, traceback, pickle
21 from twisted.application import internet, service
22 from twisted.internet import protocol, reactor, utils, defer
23 from twisted.internet.task import LoopingCall
25 class doomClientProtocol( protocol.ProcessProtocol ):
29 def connectionMade( self ):
30 self.logfile.write( 'connectionMade\n' )
32 def outReceived( self, data ):
34 self.logfile.write( data )
36 def errReceived( self, data ):
37 print 'stderr: ' + data
38 self.logfile.write( 'stderr: ' + data )
40 def inConnectionLost( self ):
41 self.logfile.write( 'inConnectionLost\n' )
43 def outConnectionLost( self ):
44 self.logfile.write( 'outConnectionLost\n' )
46 def errConnectionLost( self ):
47 self.logfile.write( 'errConnectionLost\n' )
49 def processEnded( self, status_object ):
50 self.logfile.write( 'processEnded %s\n' % repr( status_object ) )
51 self.logfile.write( time.strftime( '%H:%M:%S', time.localtime( time.time() ) ) + '\n' )
53 self.deferred.callback( None )
56 def __init__( self, logfilename, deferred ):
57 self.logfilename = logfilename
58 self.logfile = open( logfilename, 'a' )
59 self.logfile.write( time.strftime( '%H:%M:%S', time.localtime( time.time() ) ) + '\n' )
60 self.deferred = deferred
62 class doomService( service.Service ):
64 # current monitoring state
66 # 1: we have a process running, we're monitoring it's CPU usage
67 # 2: we issued a 'quit' to the process's stdin
68 # either going to get a processEnded, or a timeout
69 # 3: we forced a kill because of error, timeout etc.
76 pickle_file = 'runner.pickle'
78 # stores status indexed by filename
79 # { 'mapname' : ( state, last_update ), .. }
82 # start the maps as multiplayer server
85 def __init__( self, bin, cmdline, maps, sort = 0, multiplayer = 0, blank_run = 0 ):
86 self.p_transport = None
87 self.multiplayer = multiplayer
88 self.blank_run = blank_run
89 if ( self.multiplayer ):
90 print 'Operate in multiplayer mode'
91 self.bin = os.path.abspath( bin )
92 if ( type( cmdline ) is type( '' ) ):
93 self.cmdline = string.split( cmdline, ' ' )
95 self.cmdline = cmdline
97 if ( os.path.exists( self.pickle_file ) ):
98 print 'Loading pickled status %s' % self.pickle_file
99 handle = open( self.pickle_file, 'r' )
100 self.status = pickle.load( handle )
103 print 'Sorting maps oldest runs first'
106 i_log = self.getLogfile( i )
107 if ( os.path.exists( i_log ) ):
108 maps_sorted.append( ( i, os.path.getmtime( i_log ) ) )
110 maps_sorted.append( ( i, 0 ) )
111 maps_sorted.sort( lambda x,y : cmp( x[1], y[1] ) )
114 self.maps.append( 'blankrun' )
115 for i in maps_sorted:
116 self.maps.append( i[ 0 ] )
117 print 'Sorted as: %s\n' % repr( self.maps )
119 def getLogfile( self, name ):
120 return 'logs/' + string.translate( name, string.maketrans( '/', '-' ) ) + '.log'
122 # deferred call when child process dies
123 def processEnded( self, val ):
124 print 'child has died - state %d' % self.state
125 self.status[ self.maps[ self.i_map ] ] = ( self.state, time.time() )
127 if ( self.i_map >= len( self.maps ) ):
132 def processTimeout( self ):
133 self.p_transport.signalProcess( "KILL" )
135 def sleepAVGReply( self, val ):
138 print 'sleepAVGReply %s%%' % s
140 # need twice in a row
141 if ( self.state == 2 ):
142 print 'child process is interactive'
143 self.p_transport.write( 'quit\n' )
149 # reactor.callLater( self.check_period, self.checkCPU )
151 print traceback.format_tb( sys.exc_info()[2] )
152 print sys.exc_info()[0]
153 print 'exception raised in sleepAVGReply - killing process'
155 self.p_transport.signalProcess( 'KILL' )
157 def sleepAVGTimeout( self ):
158 print 'sleepAVGTimeout - killing process'
160 self.p_transport.signalProcess( 'KILL' )
162 # called at regular intervals to monitor the sleep average of the child process
163 # when sleep reaches 0, it means the map is loaded and interactive
164 def checkCPU( self ):
165 if ( self.state == 0 or self.p_transport is None or self.p_transport.pid is None ):
166 print 'checkCPU: no child process atm'
168 defer = utils.getProcessOutput( '/bin/bash', [ '-c', 'cat /proc/%d/status | grep SleepAVG' % self.p_transport.pid ] )
169 defer.addCallback( self.sleepAVGReply )
170 defer.setTimeout( 2, self.sleepAVGTimeout )
174 name = self.maps[ self.i_map ]
175 print 'Starting map: ' + name
176 logfile = self.getLogfile( name )
177 print 'Logging to: ' + logfile
178 if ( self.multiplayer ):
179 cmdline = [ self.bin ] + self.cmdline + [ '+set', 'si_map', name ]
180 if ( name != 'blankrun' ):
181 cmdline.append( '+spawnServer' )
183 cmdline = [ self.bin ] + self.cmdline
184 if ( name != 'blankrun' ):
185 cmdline += [ '+devmap', name ]
186 print 'Command line: ' + repr( cmdline )
187 self.deferred = defer.Deferred()
188 self.deferred.addCallback( self.processEnded )
189 self.p_transport = reactor.spawnProcess( doomClientProtocol( logfile, self.deferred ), self.bin, cmdline , path = os.path.dirname( self.bin ), env = os.environ )
191 # # setup the CPU usage loop
192 # reactor.callLater( self.check_period, self.checkCPU )
194 def startService( self ):
195 print 'doomService startService'
196 loop = LoopingCall( self.checkCPU )
197 loop.start( self.check_period )
201 def stopService( self ):
202 print 'doomService stopService'
203 if ( not self.p_transport.pid is None ):
204 self.p_transport.signalProcess( 'KILL' )
206 print 'saving status to %s' % self.pickle_file
207 handle = open( self.pickle_file, 'w+' )
208 pickle.dump( self.status, handle )