Package FuzzManager :: Package FTB :: Package Running :: Module PersistentApplication
[hide private]
[frames] | no frames]

Source Code for Module FuzzManager.FTB.Running.PersistentApplication

  1  #!/usr/bin/env python 
  2  # encoding: utf-8 
  3  ''' 
  4  PersistentApplication -- Implements a persistent application for performing 
  5  multiple tests and offers an interface to perform the necessary tasks around 
  6  testing such an application. 
  7   
  8  @author:     Christian Holler (:decoder) 
  9   
 10  @license: 
 11   
 12  This Source Code Form is subject to the terms of the Mozilla Public 
 13  License, v. 2.0. If a copy of the MPL was not distributed with this 
 14  file, You can obtain one at http://mozilla.org/MPL/2.0/. 
 15   
 16  @contact:    choller@mozilla.com 
 17  ''' 
 18   
 19  # Ensure print() compatibility with Python 3 
 20  from __future__ import print_function 
 21   
 22  from abc import ABCMeta 
 23  import subprocess 
 24  import os 
 25  import Queue 
 26  import time 
 27  import signal 
 28   
 29  from FTB.Running.StreamCollector import StreamCollector 
 30   
 31   
32 -class ApplicationStatus:
33 OK, ERROR, TIMEDOUT, CRASHED = range(1,5)
34
35 -class PersistentApplication():
36 ''' 37 Abstract base class that defines the interface 38 ''' 39 __metaclass__ = ABCMeta 40
41 - def __init__(self, binary, args=None, env=None, cwd=None):
42 self.binary = binary 43 self.cwd = cwd 44 45 # Use the system environment as a base environment 46 self.env = dict(os.environ) 47 if env: 48 for envkey in env: 49 self.env[envkey] = env[envkey] 50 51 self.args = args 52 if self.args is None: 53 self.args = [] 54 55 assert isinstance(self.env, dict) 56 assert isinstance(self.args, list) 57 58 # Various variables holding information about the program 59 self.process = None 60 self.stdout = None 61 self.stderr = None 62 self.testLog = None
63
64 - def start(self):
65 pass
66
67 - def stop(self):
68 pass
69
70 - def runTest(self, test):
71 pass
72
73 - def status(self):
74 pass
75
76 -class SimplePersistentApplication(PersistentApplication):
77 - def __init__(self, binary, args=None, env=None, cwd=None):
78 PersistentApplication.__init__(self, binary, args, env, cwd) 79 80 # How many seconds to give the program for processing out input 81 self.processingTimeout = 10
82
83 - def start(self):
84 assert self.process == None or self.process.poll() != None 85 86 # Reset the test log 87 self.testLog = [] 88 89 popenArgs = [ self.binary ] 90 popenArgs.extend(self.args) 91 92 self.process = subprocess.Popen( 93 popenArgs, 94 stdin=subprocess.PIPE, 95 stdout=subprocess.PIPE, 96 stderr=subprocess.PIPE, 97 cwd=self.cwd, 98 env=self.env, 99 universal_newlines=True 100 ) 101 102 # This queue is used to queue up responses that should be directly processed 103 # by this class rather than being logged. 104 self.responseQueue = Queue.Queue() 105 106 self.outCollector = StreamCollector(self.process.stdout, self.responseQueue, logResponses=False, maxBacklog=256) 107 self.errCollector = StreamCollector(self.process.stderr, self.responseQueue, logResponses=False, maxBacklog=256) 108 109 # Anything prefixed with "SPFP: " will be directly forwarded to us 110 self.outCollector.addResponsePrefix("SPFP: ") 111 self.errCollector.addResponsePrefix("SPFP: ") 112 113 self.outCollector.start() 114 self.errCollector.start() 115 116 try: 117 self.process.stdin.write('selftest\n') 118 except IOError: 119 raise RuntimeError("SPFP Error: Selftest failed, application did not start properly.") 120 121 try: 122 response = self.responseQueue.get(block=True, timeout=self.processingTimeout) 123 except Queue.Empty: 124 raise RuntimeError("SPFP Error: Selftest failed, no response.") 125 126 if response != "PASSED": 127 raise RuntimeError("SPFP Error: Selftest failed, unsupported application response: %s" % response)
128
129 - def stop(self):
130 self._terminateProcess() 131 132 # Ensure we leave no dangling threads when stopping 133 self.outCollector.join() 134 self.errCollector.join() 135 136 # Make the output available 137 self.stdout = self.outCollector.output 138 self.stderr = self.errCollector.output
139
140 - def runTest(self, test):
141 if self.process == None or self.process.poll() != None: 142 self.start() 143 144 self.testLog.append(test) 145 self.process.stdin.write('%s\n' % test) 146 147 try: 148 response = self.responseQueue.get(block=True, timeout=self.processingTimeout) 149 except Queue.Empty: 150 if self.process.poll() == None: 151 # The process is still running, force it to stop and return timeout code 152 self.stop() 153 return ApplicationStatus.TIMEDOUT 154 else: 155 # The process has exited. We need to check if it crashed, but first we 156 # call stop to join our collector threads. 157 self.stop() 158 159 if self.process.returncode < 0: 160 crashSignals = [ 161 # POSIX.1-1990 signals 162 signal.SIGILL, 163 signal.SIGABRT, 164 signal.SIGFPE, 165 signal.SIGSEGV, 166 # SUSv2 / POSIX.1-2001 signals 167 signal.SIGBUS, 168 signal.SIGSYS, 169 signal.SIGTRAP, 170 ] 171 172 for crashSignal in crashSignals: 173 if self.process.returncode == -crashSignal: 174 return ApplicationStatus.CRASHED 175 176 # The application was terminated by a signal, but not by one of the listed signals. 177 # We consider this a fatal error. Either the signal should be supported here, or the 178 # process is being terminated by something else, making the testing unreliable. 179 # 180 # TODO: This could be triggered by the Linux kernel OOM killer 181 raise RuntimeError("SPFP Error: Application terminated with signal: %s" % self.process.returncode) 182 else: 183 # The application exited, but didn't send us any message before doing so. We consider this 184 # a protocol violation and raise an exception. 185 raise RuntimeError("SPFP Error: Application exited without message. Exitcode: %s" % self.process.returncode) 186 187 if response == 'OK': 188 return ApplicationStatus.OK 189 elif response == 'ERROR': 190 return ApplicationStatus.ERROR 191 192 raise RuntimeError("SPFP Error: Unsupported application response: %s" % response)
193
194 - def _terminateProcess(self):
195 if self.process: 196 if self.process.poll() == None: 197 # Try to terminate the process gracefully first 198 self.process.terminate() 199 200 # Emulate a wait() with timeout. Because wait() having 201 # a timeout would be way too easy, wouldn't it? -.- 202 (maxSleepTime, pollInterval) = (3, 0.2) 203 while self.process.poll() == None and maxSleepTime > 0: 204 maxSleepTime -= pollInterval 205 time.sleep(pollInterval) 206 207 # Process is still alive, kill it and wait 208 if self.process.poll() == None: 209 self.process.kill() 210 self.process.wait()
211