1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
29 EXPR = re.compile(".*\n$")
30
31
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
80 self.pid = None
81 self.exitStatus = None
82
83
84 self.__hProcess = None
85 self.__hThread = None
86 self.__tid = None
87 self.__outQueue = Queue.Queue()
88
89
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
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
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
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
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
150 """Private method to start a process running in the background.
151
152 """
153 with process_lock:
154
155 sAttrs = win32security.SECURITY_ATTRIBUTES()
156 sAttrs.bInheritHandle = 1
157
158
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
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
175
176
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
183
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
196 self.__stdin = hStdin
197
198
199
200 if self.running():
201 thread.start_new_thread(self.__writeStdin, (hStdin, ))
202
203
205 """Private method to start a process running in the foreground.
206
207 """
208 self.__startBackgroundProcess()
209 self.wait(self.timeout)
210
211
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
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
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
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