1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
28 EXPR = re.compile(".*\n$")
29
30
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
89 self.pid = None
90 self.exitStatus = None
91
92
93 self.__outQueue = Queue.Queue()
94
95
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
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
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
134 os.chdir(self.workingDir)
135
136
137 os.dup2(stdin_r, 0)
138
139
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
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
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
162
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
173 """Private method to start a process running in the foreground.
174
175 """
176 self.__startBackgroundProcess()
177 self.wait(self.timeout)
178
179
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
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
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
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
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