Package pysys :: Package process :: Module helper
[hide private]
[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 string, os.path, time, thread, logging, Queue 
 21  import win32api, win32pdh, win32security, win32process, win32file, win32pipe, win32con, pywintypes 
 22   
 23  from pysys import log 
 24  from pysys import process_lock 
 25  from pysys.constants import * 
 26  from pysys.exceptions import * 
 27   
 28  # check for new lines on end of a string 
 29  EXPR = re.compile(".*\n$") 
 30   
 31   
32 -class ProcessWrapper:
33 """Process wrapper for process execution and management. 34 35 The process wrapper provides the ability to start and stop an external process, setting 36 the process environment, working directory and state i.e. a foreground process in which case 37 a call to the L{start} method will not return until the process has exited, or a background 38 process in which case the process is started in a separate thread allowing concurrent execution 39 within the testcase. Processes started in the foreground can have a timeout associated with them, such 40 that should the timeout be exceeded, the process will be terminated and control passed back to the 41 caller of the method. The wrapper additionally allows control over logging of the process stdout 42 and stderr to file, and writing to the process stdin. 43 44 Usage of the class is to first create an instance, setting all runtime parameters of the process 45 as data attributes to the class instance via the constructor. The process can then be started 46 and stopped via the L{start} and L{stop} methods of the class, as well as interrogated for 47 its executing status via the L{running} method, and waited for its completion via the L{wait} 48 method. During process execution the C{self.pid} and C{seld.exitStatus} data attributes are set 49 within the class instance, and these values can be accessed directly via it's object reference. 50 51 @ivar pid: The process id for a running or complete process (as set by the OS) 52 @type pid: integer 53 @ivar exitStatus: The process exit status for a completed process 54 @type exitStatus: integer 55 56 """ 57
58 - def __init__(self, command, arguments, environs, workingDir, state, timeout, stdout=None, stderr=None):
59 """Create an instance of the process wrapper. 60 61 @param command: The full path to the command to execute 62 @param arguments: A list of arguments to the command 63 @param environs: A dictionary of environment variables (key, value) for the process context execution 64 @param workingDir: The working directory for the process 65 @param state: The state of the process (L{pysys.constants.FOREGROUND} or L{pysys.constants.BACKGROUND} 66 @param timeout: The timeout in seconds to be applied to the process 67 @param stdout: The full path to the filename to write the stdout of the process 68 @param stderr: The full path to the filename to write the sdterr of the process 69 70 """ 71 self.command = command 72 self.arguments = arguments 73 self.environs = {} 74 for key in environs: self.environs[self.__stringToUnicode(key)] = self.__stringToUnicode(environs[key]) 75 self.workingDir = workingDir 76 self.state = state 77 self.timeout = timeout 78 79 # 'publicly' available data attributes set on execution 80 self.pid = None 81 self.exitStatus = None 82 83 # private instance variables 84 self.__hProcess = None 85 self.__hThread = None 86 self.__tid = None 87 self.__outQueue = Queue.Queue() 88 89 # set the stdout|err file handles 90 self.fStdout = 'nul' 91 self.fStderr = 'nul' 92 try: 93 if stdout is not None: self.fStdout = self.__stringToUnicode(stdout) 94 except: 95 log.info("Unable to create file to capture stdout - using the null device") 96 try: 97 if stderr is not None: self.fStderr = self.__stringToUnicode(stderr) 98 except: 99 log.info("Unable to create file to capture stdout - using the null device") 100 101 # print process debug information 102 log.debug("Process parameters for executable %s" % os.path.basename(self.command)) 103 log.debug(" command : %s", self.command) 104 for a in self.arguments: log.debug(" argument : %s", a) 105 log.debug(" working dir : %s", self.workingDir) 106 log.debug(" stdout : %s", stdout) 107 log.debug(" stdout : %s", stderr) 108 keys=self.environs.keys() 109 keys.sort() 110 for e in keys: log.debug(" environment : %s=%s", e, self.environs[e])
111 112
113 - def __stringToUnicode(self, s):
114 """ Converts a unicode string or a utf-8 bit string into a unicode string. 115 116 """ 117 if isinstance(s, unicode): 118 return s 119 else: 120 return unicode(s, "utf8")
121 122
123 - def __writeStdin(self, hStdin):
124 """Private method to write to the process stdin pipe. 125 126 """ 127 while 1: 128 try: 129 data = self.__outQueue.get(block=True, timeout=0.25) 130 except Queue.Empty: 131 if not self.running(): 132 win32file.CloseHandle(hStdin) 133 break 134 else: 135 win32file.WriteFile(hStdin, data, None)
136 137
138 - def __quotePath(self, input):
139 """Private method to sanitise a windows path. 140 141 """ 142 i = input 143 if i.find(' ') > 0: 144 return '\"%s\"' % i 145 else: 146 return i
147 148
149 - def __startBackgroundProcess(self):
150 """Private method to start a process running in the background. 151 152 """ 153 with process_lock: 154 # security attributes for pipes 155 sAttrs = win32security.SECURITY_ATTRIBUTES() 156 sAttrs.bInheritHandle = 1 157 158 # create pipes for the process to write to 159 hStdin_r, hStdin = win32pipe.CreatePipe(sAttrs, 0) 160 hStdout = win32file.CreateFile(self.__stringToUnicode(self.fStdout), win32file.GENERIC_WRITE | win32file.GENERIC_READ, 161 win32file.FILE_SHARE_DELETE | win32file.FILE_SHARE_READ | win32file.FILE_SHARE_WRITE, 162 sAttrs, win32file.CREATE_ALWAYS, win32file.FILE_ATTRIBUTE_NORMAL, None) 163 hStderr = win32file.CreateFile(self.__stringToUnicode(self.fStderr), win32file.GENERIC_WRITE | win32file.GENERIC_READ, 164 win32file.FILE_SHARE_DELETE | win32file.FILE_SHARE_READ | win32file.FILE_SHARE_WRITE, 165 sAttrs, win32file.CREATE_ALWAYS, win32file.FILE_ATTRIBUTE_NORMAL, None) 166 167 # set the info structure for the new process. 168 StartupInfo = win32process.STARTUPINFO() 169 StartupInfo.hStdInput = hStdin_r 170 StartupInfo.hStdOutput = hStdout 171 StartupInfo.hStdError = hStderr 172 StartupInfo.dwFlags = win32process.STARTF_USESTDHANDLES 173 174 # Create new handles for the thread ends of the pipes. The duplicated handles will 175 # have their inheritence properties set to false so that any children inheriting these 176 # handles will not have non-closeable handles to the pipes 177 pid = win32api.GetCurrentProcess() 178 tmp = win32api.DuplicateHandle(pid, hStdin, pid, 0, 0, win32con.DUPLICATE_SAME_ACCESS) 179 win32file.CloseHandle(hStdin) 180 hStdin = tmp 181 182 # start the process, and close down the copies of the process handles 183 # we have open after the process creation (no longer needed here) 184 old_command = command = self.__quotePath(self.command) 185 for arg in self.arguments: command = '%s %s' % (command, self.__quotePath(arg)) 186 try: 187 self.__hProcess, self.__hThread, self.pid, self.__tid = win32process.CreateProcess( None, command, None, None, 1, 0, self.environs, os.path.normpath(self.workingDir), StartupInfo) 188 except pywintypes.error: 189 raise ProcessError, "Error creating process %s" % (old_command) 190 191 win32file.CloseHandle(hStdin_r) 192 win32file.CloseHandle(hStdout) 193 win32file.CloseHandle(hStderr) 194 195 # set the handle to the stdin of the process 196 self.__stdin = hStdin 197 198 # check to see if the process is running. If it is kick off the threads to collect 199 # the stdout and stderr 200 if self.running(): 201 thread.start_new_thread(self.__writeStdin, (hStdin, ))
202 203
204 - def __startForegroundProcess(self):
205 """Private method to start a process running in the foreground. 206 207 """ 208 self.__startBackgroundProcess() 209 self.wait(self.timeout)
210 211
212 - def __setExitStatus(self):
213 """Private method to set the exit status of the process. 214 215 """ 216 if self.exitStatus is not None: return 217 try: 218 exitStatus = win32process.GetExitCodeProcess(self.__hProcess) 219 if exitStatus != win32con.STILL_ACTIVE: 220 win32file.CloseHandle(self.__hProcess) 221 win32file.CloseHandle(self.__hThread) 222 self.__outQueue = None 223 self.exitStatus = exitStatus 224 except pywintypes.error as ex: 225 self.__outQueue = None 226 self.exitStatus = win32con.STATUS_INVALID_HANDLE 227 log.warning(ex)
228 229
230 - def write(self, data, addNewLine=True):
231 """Write data to the stdin of the process. 232 233 Note that when the addNewLine argument is set to true, if a new line does not 234 terminate the input data string, a newline character will be added. If one 235 already exists a new line character will not be added. Should you explicitly 236 require to add data without the method appending a new line charater set 237 addNewLine to false. 238 239 @param data: The data to write to the process stdout 240 @param addNewLine: True if a new line character is to be added to the end of 241 the data string 242 243 """ 244 if addNewLine and not EXPR.search(data): data = "%s\n" % data 245 self.__outQueue.put(data)
246 247
248 - def running(self):
249 """Check to see if a process is running, returning true if running. 250 251 @return: The running status (True / False) 252 @rtype: integer 253 254 """ 255 self.__setExitStatus() 256 if self.exitStatus is not None: return False 257 return True
258 259
260 - def wait(self, timeout):
261 """Wait for a process to complete execution. 262 263 The method will block until either the process is no longer running, or the timeout 264 is exceeded. Note that the method will not terminate the process if the timeout is 265 exceeded. 266 267 @param timeout: The timeout to wait in seconds 268 @raise ProcessTimeout: Raised if the timeout is exceeded. 269 270 """ 271 startTime = time.time() 272 while self.running(): 273 if timeout: 274 currentTime = time.time() 275 if currentTime > startTime + timeout: 276 raise ProcessTimeout, "Process timedout" 277 time.sleep(0.1)
278 279
280 - def stop(self, timeout=TIMEOUTS['WaitForProcessStop']):
281 """Stop a process running. 282 283 @raise ProcessError: Raised if an error occurred whilst trying to stop the process 284 285 """ 286 if self.exitStatus is not None: return 287 try: 288 win32api.TerminateProcess(self.__hProcess,0) 289 self.wait(timeout=timeout) 290 except: 291 raise ProcessError, "Error stopping process"
292 293
294 - def signal(self, signal):
295 """Send a signal to a running process. 296 297 Note that this method is not implemented for win32 processes, and calling this on a 298 win32 OS will raise a NotImplementedError. 299 300 @param signal: The integer signal to send to the process 301 @raise ProcessError: Raised if an error occurred whilst trying to signal the process 302 303 """ 304 raise NotImplementedError , "Unable to send a signal to a windows process"
305 306
307 - def start(self):
308 """Start a process using the runtime parameters set at instantiation. 309 310 @raise ProcessError: Raised if there is an error creating the process 311 @raise ProcessTimeout: Raised in the process timed out (foreground process only) 312 313 """ 314 if self.state == FOREGROUND: 315 self.__startForegroundProcess() 316 else: 317 self.__startBackgroundProcess() 318 time.sleep(1)
319