Package pysys :: Package process :: Module helper
[frames] | no frames]

Source Code for Module pysys.process.helper

  1  #!/usr/bin/env python 
  2  # PySys System Test Framework, Copyright (C) 2006-2013  M.B.Grieve 
  3   
  4  # This library is free software; you can redistribute it and/or 
  5  # modify it under the terms of the GNU Lesser General Public 
  6  # License as published by the Free Software Foundation; either 
  7  # version 2.1 of the License, or (at your option) any later version. 
  8   
  9  # This library is distributed in the hope that it will be useful, 
 10  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 11  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
 12  # Lesser General Public License for more details. 
 13   
 14  # You should have received a copy of the GNU Lesser General Public 
 15  # License along with this library; if not, write to the Free Software 
 16  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 
 17   
 18  # Contact: moraygrieve@users.sourceforge.net 
 19   
 20  import signal, time, copy, logging, Queue, thread, errno 
 21   
 22  from pysys import log 
 23  from pysys import process_lock 
 24  from pysys.constants import * 
 25  from pysys.exceptions import * 
 26   
 27  # check for new lines on end of a string 
 28  EXPR = re.compile(".*\n$") 
 29   
 30   
31 -class ProcessWrapper:
32 """Unix process wrapper for process execution and management. 33 34 The unix process wrapper provides the ability to start and stop an external process, setting 35 the process environment, working directory and state i.e. a foreground process in which case 36 a call to the L{start()} method will not return until the process has exited, or a background 37 process in which case the process is started in a separate thread allowing concurrent execution 38 within the testcase. Processes started in the foreground can have a timeout associated with them, such 39 that should the timeout be exceeded, the process will be terminated and control passed back to the 40 caller of the method. The wrapper additionally allows control over logging of the process stdout 41 and stderr to file, and writing to the process stdin. 42 43 Usage of the class is to first create an instance, setting all runtime parameters of the process 44 as data attributes to the class instance via the constructor. The process can then be started 45 and stopped via the L{start()} and L{stop()} methods of the class, as well as interrogated for 46 its executing status via the L{running()} method, and waited for its completion via the L{wait()} 47 method. During process execution the C{self.pid} and C{seld.exitStatus} data attributes are set 48 within the class instance, and these values can be accessed directly via it's object reference. 49 50 @ivar pid: The process id for a running or complete process (as set by the OS) 51 @type pid: integer 52 @ivar exitStatus: The process exit status for a completed process 53 @type exitStatus: integer 54 55 """ 56
57 - def __init__(self, command, arguments, environs, workingDir, state, timeout, stdout=None, stderr=None):
58 """Create an instance of the process wrapper. 59 60 @param command: The full path to the command to execute 61 @param arguments: A list of arguments to the command 62 @param environs: A dictionary of environment variables (key, value) for the process context execution 63 @param workingDir: The working directory for the process 64 @param state: The state of the process (L{pysys.constants.FOREGROUND} or L{pysys.constants.BACKGROUND} 65 @param timeout: The timeout in seconds to be applied to the process 66 @param stdout: The full path to the filename to write the stdout of the process 67 @param stderr: The full path to the filename to write the sdterr of the process 68 69 """ 70 self.command = command 71 self.arguments = arguments 72 self.environs = environs 73 self.workingDir = workingDir 74 self.state = state 75 self.timeout = timeout 76 77 self.stdout = '/dev/null' 78 self.stderr = '/dev/null' 79 try: 80 if stdout is not None: self.stdout = stdout 81 except: 82 log.info('Unable to create file to capture stdout - using the null device') 83 try: 84 if stderr is not None: self.stderr = stderr 85 except: 86 log.info('Unable to create file to capture stdout - using the null device') 87 88 # 'publicly' available data attributes set on execution 89 self.pid = None 90 self.exitStatus = None 91 92 # private instance variables 93 self.__outQueue = Queue.Queue() 94 95 # print process debug information 96 log.debug("Process parameters for executable %s" % os.path.basename(self.command)) 97 log.debug(" command : %s", self.command) 98 for a in self.arguments: log.debug(" argument : %s", a) 99 log.debug(" working dir : %s", self.workingDir) 100 log.debug(" stdout : %s", self.stdout) 101 log.debug(" stdout : %s", self.stderr) 102 keys=self.environs.keys() 103 keys.sort() 104 for e in keys: log.debug(" environment : %s=%s", e, self.environs[e])
105 106
107 - def __writeStdin(self, fd):
108 """Private method to write to the process stdin pipe. 109 110 """ 111 while 1: 112 try: 113 data = self.__outQueue.get(block=True, timeout=0.25) 114 except Queue.Empty: 115 if not self.running(): 116 os.close(fd) 117 break 118 else: 119 os.write(fd, data)
120 121
122 - def __startBackgroundProcess(self):
123 """Private method to start a process running in the background. 124 125 """ 126 with process_lock: 127 128 try: 129 stdin_r, stdin_w = os.pipe() 130 self.pid = os.fork() 131 132 if self.pid == 0: 133 # change working directory of the child process 134 os.chdir(self.workingDir) 135 136 # duplicate the read end of the pipe to stdin 137 os.dup2(stdin_r, 0) 138 139 # create and duplicate stdout and stderr to open file handles 140 stdout_w = os.open(self.stdout, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) 141 stderr_w = os.open(self.stderr, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) 142 os.dup2(stdout_w, 1) 143 os.dup2(stderr_w, 2) 144 145 # close any stray file descriptors (within reason) 146 try: 147 maxfd = os.sysconf("SC_OPEN_MAX") 148 except: 149 maxfd=256 150 for fd in range(3, maxfd): 151 try: 152 os.close(fd) 153 except: 154 pass 155 156 # execve the process to start it 157 arguments = copy.copy(self.arguments) 158 arguments.insert(0, os.path.basename(self.command)) 159 os.execve(self.command, arguments, self.environs) 160 else: 161 # close the read end of the pipe in the parent 162 # and start a thread to write to the write end 163 os.close(stdin_r) 164 thread.start_new_thread(self.__writeStdin, (stdin_w, )) 165 except: 166 if self.pid == 0: os._exit(os.EX_OSERR) 167 168 if not self.running() and self.exitStatus == os.EX_OSERR: 169 raise ProcessError, "Error creating process %s" % (self.command)
170 171
172 - def __startForegroundProcess(self):
173 """Private method to start a process running in the foreground. 174 175 """ 176 self.__startBackgroundProcess() 177 self.wait(self.timeout)
178 179
180 - def __setExitStatus(self):
181 """Private method to set the exit status of the process. 182 183 """ 184 if self.exitStatus is not None: return 185 186 retries = 3 187 while retries > 0: 188 try: 189 pid, status = os.waitpid(self.pid, os.WNOHANG) 190 if pid == self.pid: 191 if os.WIFEXITED(status): 192 self.exitStatus = os.WEXITSTATUS(status) 193 elif os.WIFSIGNALED(status): 194 self.exitStatus = os.WTERMSIG(status) 195 else: 196 self.exitStatus = status 197 self.__outQueue = None 198 retries=0 199 except OSError, e: 200 if e.errno == errno.ECHILD: 201 time.sleep(0.01) 202 retries=retries-1 203 else: 204 retries=0
205 206
207 - def write(self, data, addNewLine=True):
208 """Write data to the stdin of the process. 209 210 Note that when the addNewLine argument is set to true, if a new line does not 211 terminate the input data string, a newline character will be added. If one 212 already exists a new line character will not be added. Should you explicitly 213 require to add data without the method appending a new line charater set 214 addNewLine to false. 215 216 @param data: The data to write to the process stdout 217 @param addNewLine: True if a new line character is to be added to the end of 218 the data string 219 220 """ 221 if addNewLine and not EXPR.search(data): data = "%s\n" % data 222 self.__outQueue.put(data)
223 224
225 - def running(self):
226 """Check to see if a process is running, returning true if running. 227 228 @return: The running status (True / False) 229 @rtype: integer 230 231 """ 232 self.__setExitStatus() 233 if self.exitStatus is not None: return 0 234 return 1
235 236
237 - def wait(self, timeout):
238 """Wait for a process to complete execution. 239 240 The method will block until either the process is no longer running, or the timeout 241 is exceeded. Note that the method will not terminate the process if the timeout is 242 exceeded. 243 244 @param timeout: The timeout to wait in seconds 245 @raise ProcessTimeout: Raised if the timeout is exceeded. 246 247 """ 248 startTime = time.time() 249 while self.running(): 250 if timeout: 251 currentTime = time.time() 252 if currentTime > startTime + timeout: 253 raise ProcessTimeout, "Process timedout" 254 time.sleep(0.1)
255 256
257 - def stop(self, timeout=TIMEOUTS['WaitForProcessStop']):
258 """Stop a process running. 259 260 @raise ProcessError: Raised if an error occurred whilst trying to stop the process 261 262 """ 263 if self.exitStatus is not None: return 264 try: 265 os.kill(self.pid, signal.SIGTERM) 266 self.wait(timeout=timeout) 267 except: 268 raise ProcessError, "Error stopping process"
269 270
271 - def signal(self, signal):
272 """Send a signal to a running process. 273 274 @param signal: The integer signal to send to the process 275 @raise ProcessError: Raised if an error occurred whilst trying to signal the process 276 277 """ 278 try: 279 os.kill(self.pid, signal) 280 except: 281 raise ProcessError, "Error signaling process"
282 283
284 - def start(self):
285 """Start a process using the runtime parameters set at instantiation. 286 287 @raise ProcessError: Raised if there is an error creating the process 288 @raise ProcessTimeout: Raised in the process timed out (foreground process only) 289 290 """ 291 if self.state == FOREGROUND: 292 self.__startForegroundProcess() 293 else: 294 self.__startBackgroundProcess()
295