runner_lib.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  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 )
  4. # currently uses a basic stdout activity timeout to decide when to move on
  5. # using a periodic check of /proc/<pid>/status SleepAVG
  6. # when the sleep average is reaching 0, issue a 'quit' to stdout
  7. # keeps serialized run status in runner.pickle
  8. # NOTE: can be used to initiate runs on failed maps only for instance etc.
  9. # TODO: use the serialized and not the logs to sort the run order
  10. # TODO: better logging. Use idLogger?
  11. # TODO: configurable event when the process is found interactive
  12. # instead of emitting a quit, perform some warning action?
  13. import sys, os, commands, string, time, traceback, pickle
  14. from twisted.application import internet, service
  15. from twisted.internet import protocol, reactor, utils, defer
  16. from twisted.internet.task import LoopingCall
  17. class doomClientProtocol( protocol.ProcessProtocol ):
  18. # ProcessProtocol API
  19. def connectionMade( self ):
  20. self.logfile.write( 'connectionMade\n' )
  21. def outReceived( self, data ):
  22. print data
  23. self.logfile.write( data )
  24. def errReceived( self, data ):
  25. print 'stderr: ' + data
  26. self.logfile.write( 'stderr: ' + data )
  27. def inConnectionLost( self ):
  28. self.logfile.write( 'inConnectionLost\n' )
  29. def outConnectionLost( self ):
  30. self.logfile.write( 'outConnectionLost\n' )
  31. def errConnectionLost( self ):
  32. self.logfile.write( 'errConnectionLost\n' )
  33. def processEnded( self, status_object ):
  34. self.logfile.write( 'processEnded %s\n' % repr( status_object ) )
  35. self.logfile.write( time.strftime( '%H:%M:%S', time.localtime( time.time() ) ) + '\n' )
  36. self.logfile.close()
  37. self.deferred.callback( None )
  38. # mac management
  39. def __init__( self, logfilename, deferred ):
  40. self.logfilename = logfilename
  41. self.logfile = open( logfilename, 'a' )
  42. self.logfile.write( time.strftime( '%H:%M:%S', time.localtime( time.time() ) ) + '\n' )
  43. self.deferred = deferred
  44. class doomService( service.Service ):
  45. # current monitoring state
  46. # 0: nothing running
  47. # 1: we have a process running, we're monitoring it's CPU usage
  48. # 2: we issued a 'quit' to the process's stdin
  49. # either going to get a processEnded, or a timeout
  50. # 3: we forced a kill because of error, timeout etc.
  51. state = 0
  52. # load check period
  53. check_period = 10
  54. # pickled status file
  55. pickle_file = 'runner.pickle'
  56. # stores status indexed by filename
  57. # { 'mapname' : ( state, last_update ), .. }
  58. status = {}
  59. # start the maps as multiplayer server
  60. multiplayer = 0
  61. def __init__( self, bin, cmdline, maps, sort = 0, multiplayer = 0, blank_run = 0 ):
  62. self.p_transport = None
  63. self.multiplayer = multiplayer
  64. self.blank_run = blank_run
  65. if ( self.multiplayer ):
  66. print 'Operate in multiplayer mode'
  67. self.bin = os.path.abspath( bin )
  68. if ( type( cmdline ) is type( '' ) ):
  69. self.cmdline = string.split( cmdline, ' ' )
  70. else:
  71. self.cmdline = cmdline
  72. self.maps = maps
  73. if ( os.path.exists( self.pickle_file ) ):
  74. print 'Loading pickled status %s' % self.pickle_file
  75. handle = open( self.pickle_file, 'r' )
  76. self.status = pickle.load( handle )
  77. handle.close()
  78. if ( sort ):
  79. print 'Sorting maps oldest runs first'
  80. maps_sorted = [ ]
  81. for i in self.maps:
  82. i_log = self.getLogfile( i )
  83. if ( os.path.exists( i_log ) ):
  84. maps_sorted.append( ( i, os.path.getmtime( i_log ) ) )
  85. else:
  86. maps_sorted.append( ( i, 0 ) )
  87. maps_sorted.sort( lambda x,y : cmp( x[1], y[1] ) )
  88. self.maps = [ ]
  89. if ( blank_run ):
  90. self.maps.append( 'blankrun' )
  91. for i in maps_sorted:
  92. self.maps.append( i[ 0 ] )
  93. print 'Sorted as: %s\n' % repr( self.maps )
  94. def getLogfile( self, name ):
  95. return 'logs/' + string.translate( name, string.maketrans( '/', '-' ) ) + '.log'
  96. # deferred call when child process dies
  97. def processEnded( self, val ):
  98. print 'child has died - state %d' % self.state
  99. self.status[ self.maps[ self.i_map ] ] = ( self.state, time.time() )
  100. self.i_map += 1
  101. if ( self.i_map >= len( self.maps ) ):
  102. reactor.stop()
  103. else:
  104. self.nextMap()
  105. def processTimeout( self ):
  106. self.p_transport.signalProcess( "KILL" )
  107. def sleepAVGReply( self, val ):
  108. try:
  109. s = val[10:][:-2]
  110. print 'sleepAVGReply %s%%' % s
  111. if ( s == '0' ):
  112. # need twice in a row
  113. if ( self.state == 2 ):
  114. print 'child process is interactive'
  115. self.p_transport.write( 'quit\n' )
  116. else:
  117. self.state = 2
  118. else:
  119. self.state = 1
  120. # else:
  121. # reactor.callLater( self.check_period, self.checkCPU )
  122. except:
  123. print traceback.format_tb( sys.exc_info()[2] )
  124. print sys.exc_info()[0]
  125. print 'exception raised in sleepAVGReply - killing process'
  126. self.state = 3
  127. self.p_transport.signalProcess( 'KILL' )
  128. def sleepAVGTimeout( self ):
  129. print 'sleepAVGTimeout - killing process'
  130. self.state = 3
  131. self.p_transport.signalProcess( 'KILL' )
  132. # called at regular intervals to monitor the sleep average of the child process
  133. # when sleep reaches 0, it means the map is loaded and interactive
  134. def checkCPU( self ):
  135. if ( self.state == 0 or self.p_transport is None or self.p_transport.pid is None ):
  136. print 'checkCPU: no child process atm'
  137. return
  138. defer = utils.getProcessOutput( '/bin/bash', [ '-c', 'cat /proc/%d/status | grep SleepAVG' % self.p_transport.pid ] )
  139. defer.addCallback( self.sleepAVGReply )
  140. defer.setTimeout( 2, self.sleepAVGTimeout )
  141. def nextMap( self ):
  142. self.state = 0
  143. name = self.maps[ self.i_map ]
  144. print 'Starting map: ' + name
  145. logfile = self.getLogfile( name )
  146. print 'Logging to: ' + logfile
  147. if ( self.multiplayer ):
  148. cmdline = [ self.bin ] + self.cmdline + [ '+set', 'si_map', name ]
  149. if ( name != 'blankrun' ):
  150. cmdline.append( '+spawnServer' )
  151. else:
  152. cmdline = [ self.bin ] + self.cmdline
  153. if ( name != 'blankrun' ):
  154. cmdline += [ '+devmap', name ]
  155. print 'Command line: ' + repr( cmdline )
  156. self.deferred = defer.Deferred()
  157. self.deferred.addCallback( self.processEnded )
  158. self.p_transport = reactor.spawnProcess( doomClientProtocol( logfile, self.deferred ), self.bin, cmdline , path = os.path.dirname( self.bin ), env = os.environ )
  159. self.state = 1
  160. # # setup the CPU usage loop
  161. # reactor.callLater( self.check_period, self.checkCPU )
  162. def startService( self ):
  163. print 'doomService startService'
  164. loop = LoopingCall( self.checkCPU )
  165. loop.start( self.check_period )
  166. self.i_map = 0
  167. self.nextMap()
  168. def stopService( self ):
  169. print 'doomService stopService'
  170. if ( not self.p_transport.pid is None ):
  171. self.p_transport.signalProcess( 'KILL' )
  172. # serialize
  173. print 'saving status to %s' % self.pickle_file
  174. handle = open( self.pickle_file, 'w+' )
  175. pickle.dump( self.status, handle )
  176. handle.close()