Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 1 | # Copyright (C) 2010 Google Inc. All rights reserved. |
| 2 | # |
| 3 | # Redistribution and use in source and binary forms, with or without |
| 4 | # modification, are permitted provided that the following conditions are |
| 5 | # met: |
| 6 | # |
| 7 | # * Redistributions of source code must retain the above copyright |
| 8 | # notice, this list of conditions and the following disclaimer. |
| 9 | # * Redistributions in binary form must reproduce the above |
| 10 | # copyright notice, this list of conditions and the following disclaimer |
| 11 | # in the documentation and/or other materials provided with the |
| 12 | # distribution. |
| 13 | # * Neither the Google name nor the names of its |
| 14 | # contributors may be used to endorse or promote products derived from |
| 15 | # this software without specific prior written permission. |
| 16 | # |
| 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 18 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 19 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 20 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 21 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 22 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 23 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 24 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 25 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 27 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 28 | |
| 29 | """Package that implements the ServerProcess wrapper class""" |
| 30 | |
| 31 | import errno |
| 32 | import logging |
| 33 | import signal |
| 34 | import sys |
| 35 | import time |
| 36 | |
| 37 | # Note that although win32 python does provide an implementation of |
| 38 | # the win32 select API, it only works on sockets, and not on the named pipes |
| 39 | # used by subprocess, so we have to use the native APIs directly. |
| 40 | if sys.platform == 'win32': |
| 41 | import msvcrt |
| 42 | import win32pipe |
| 43 | import win32file |
| 44 | else: |
| 45 | import fcntl |
| 46 | import os |
| 47 | import select |
| 48 | |
| 49 | from webkitpy.common.system.executive import ScriptError |
| 50 | |
| 51 | |
| 52 | _log = logging.getLogger(__name__) |
| 53 | |
| 54 | |
| 55 | class ServerProcess(object): |
| 56 | """This class provides a wrapper around a subprocess that |
| 57 | implements a simple request/response usage model. The primary benefit |
| 58 | is that reading responses takes a deadline, so that we don't ever block |
| 59 | indefinitely. The class also handles transparently restarting processes |
| 60 | as necessary to keep issuing commands.""" |
| 61 | |
| 62 | def __init__(self, port_obj, name, cmd, env=None, universal_newlines=False, treat_no_data_as_crash=False): |
| 63 | self._port = port_obj |
Torne (Richard Coles) | f5e4ad5 | 2013-08-05 13:57:57 +0100 | [diff] [blame] | 64 | self._name = name # Should be the command name (e.g. content_shell, image_diff) |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 65 | self._cmd = cmd |
| 66 | self._env = env |
| 67 | # Set if the process outputs non-standard newlines like '\r\n' or '\r'. |
| 68 | # Don't set if there will be binary data or the data must be ASCII encoded. |
| 69 | self._universal_newlines = universal_newlines |
| 70 | self._treat_no_data_as_crash = treat_no_data_as_crash |
| 71 | self._host = self._port.host |
| 72 | self._pid = None |
| 73 | self._reset() |
| 74 | |
| 75 | # See comment in imports for why we need the win32 APIs and can't just use select. |
| 76 | # FIXME: there should be a way to get win32 vs. cygwin from platforminfo. |
| 77 | self._use_win32_apis = sys.platform == 'win32' |
| 78 | |
| 79 | def name(self): |
| 80 | return self._name |
| 81 | |
| 82 | def pid(self): |
| 83 | return self._pid |
| 84 | |
| 85 | def _reset(self): |
| 86 | if getattr(self, '_proc', None): |
| 87 | if self._proc.stdin: |
| 88 | self._proc.stdin.close() |
| 89 | self._proc.stdin = None |
| 90 | if self._proc.stdout: |
| 91 | self._proc.stdout.close() |
| 92 | self._proc.stdout = None |
| 93 | if self._proc.stderr: |
| 94 | self._proc.stderr.close() |
| 95 | self._proc.stderr = None |
| 96 | |
| 97 | self._proc = None |
| 98 | self._output = str() # bytesarray() once we require Python 2.6 |
| 99 | self._error = str() # bytesarray() once we require Python 2.6 |
| 100 | self._crashed = False |
| 101 | self.timed_out = False |
| 102 | |
| 103 | def process_name(self): |
| 104 | return self._name |
| 105 | |
| 106 | def _start(self): |
| 107 | if self._proc: |
| 108 | raise ValueError("%s already running" % self._name) |
| 109 | self._reset() |
| 110 | # close_fds is a workaround for http://bugs.python.org/issue2320 |
| 111 | close_fds = not self._host.platform.is_win() |
| 112 | self._proc = self._host.executive.popen(self._cmd, stdin=self._host.executive.PIPE, |
| 113 | stdout=self._host.executive.PIPE, |
| 114 | stderr=self._host.executive.PIPE, |
| 115 | close_fds=close_fds, |
| 116 | env=self._env, |
| 117 | universal_newlines=self._universal_newlines) |
| 118 | self._pid = self._proc.pid |
| 119 | fd = self._proc.stdout.fileno() |
| 120 | if not self._use_win32_apis: |
| 121 | fl = fcntl.fcntl(fd, fcntl.F_GETFL) |
| 122 | fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) |
| 123 | fd = self._proc.stderr.fileno() |
| 124 | fl = fcntl.fcntl(fd, fcntl.F_GETFL) |
| 125 | fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) |
| 126 | |
| 127 | def _handle_possible_interrupt(self): |
| 128 | """This routine checks to see if the process crashed or exited |
| 129 | because of a keyboard interrupt and raises KeyboardInterrupt |
| 130 | accordingly.""" |
| 131 | # FIXME: Linux and Mac set the returncode to -signal.SIGINT if a |
| 132 | # subprocess is killed with a ctrl^C. Previous comments in this |
| 133 | # routine said that supposedly Windows returns 0xc000001d, but that's not what |
| 134 | # -1073741510 evaluates to. Figure out what the right value is |
| 135 | # for win32 and cygwin here ... |
| 136 | if self._proc.returncode in (-1073741510, -signal.SIGINT): |
| 137 | raise KeyboardInterrupt |
| 138 | |
| 139 | def poll(self): |
| 140 | """Check to see if the underlying process is running; returns None |
| 141 | if it still is (wrapper around subprocess.poll).""" |
| 142 | if self._proc: |
| 143 | return self._proc.poll() |
| 144 | return None |
| 145 | |
| 146 | def write(self, bytes): |
| 147 | """Write a request to the subprocess. The subprocess is (re-)start()'ed |
| 148 | if is not already running.""" |
| 149 | if not self._proc: |
| 150 | self._start() |
| 151 | try: |
| 152 | self._proc.stdin.write(bytes) |
| 153 | except IOError, e: |
| 154 | self.stop(0.0) |
| 155 | # stop() calls _reset(), so we have to set crashed to True after calling stop(). |
| 156 | self._crashed = True |
| 157 | |
| 158 | def _pop_stdout_line_if_ready(self): |
| 159 | index_after_newline = self._output.find('\n') + 1 |
| 160 | if index_after_newline > 0: |
| 161 | return self._pop_output_bytes(index_after_newline) |
| 162 | return None |
| 163 | |
| 164 | def _pop_stderr_line_if_ready(self): |
| 165 | index_after_newline = self._error.find('\n') + 1 |
| 166 | if index_after_newline > 0: |
| 167 | return self._pop_error_bytes(index_after_newline) |
| 168 | return None |
| 169 | |
| 170 | def pop_all_buffered_stderr(self): |
| 171 | return self._pop_error_bytes(len(self._error)) |
| 172 | |
| 173 | def read_stdout_line(self, deadline): |
| 174 | return self._read(deadline, self._pop_stdout_line_if_ready) |
| 175 | |
| 176 | def read_stderr_line(self, deadline): |
| 177 | return self._read(deadline, self._pop_stderr_line_if_ready) |
| 178 | |
| 179 | def read_either_stdout_or_stderr_line(self, deadline): |
| 180 | def retrieve_bytes_from_buffers(): |
| 181 | stdout_line = self._pop_stdout_line_if_ready() |
| 182 | if stdout_line: |
| 183 | return stdout_line, None |
| 184 | stderr_line = self._pop_stderr_line_if_ready() |
| 185 | if stderr_line: |
| 186 | return None, stderr_line |
| 187 | return None # Instructs the caller to keep waiting. |
| 188 | |
| 189 | return_value = self._read(deadline, retrieve_bytes_from_buffers) |
| 190 | # FIXME: This is a bit of a hack around the fact that _read normally only returns one value, but this caller wants it to return two. |
| 191 | if return_value is None: |
| 192 | return None, None |
| 193 | return return_value |
| 194 | |
| 195 | def read_stdout(self, deadline, size): |
| 196 | if size <= 0: |
| 197 | raise ValueError('ServerProcess.read() called with a non-positive size: %d ' % size) |
| 198 | |
| 199 | def retrieve_bytes_from_stdout_buffer(): |
| 200 | if len(self._output) >= size: |
| 201 | return self._pop_output_bytes(size) |
| 202 | return None |
| 203 | |
| 204 | return self._read(deadline, retrieve_bytes_from_stdout_buffer) |
| 205 | |
| 206 | def _log(self, message): |
| 207 | # This is a bit of a hack, but we first log a blank line to avoid |
| 208 | # messing up the master process's output. |
| 209 | _log.info('') |
| 210 | _log.info(message) |
| 211 | |
| 212 | def _handle_timeout(self): |
| 213 | self.timed_out = True |
| 214 | self._port.sample_process(self._name, self._proc.pid) |
| 215 | |
| 216 | def _split_string_after_index(self, string, index): |
| 217 | return string[:index], string[index:] |
| 218 | |
| 219 | def _pop_output_bytes(self, bytes_count): |
| 220 | output, self._output = self._split_string_after_index(self._output, bytes_count) |
| 221 | return output |
| 222 | |
| 223 | def _pop_error_bytes(self, bytes_count): |
| 224 | output, self._error = self._split_string_after_index(self._error, bytes_count) |
| 225 | return output |
| 226 | |
| 227 | def _wait_for_data_and_update_buffers_using_select(self, deadline, stopping=False): |
| 228 | if self._proc.stdout.closed or self._proc.stderr.closed: |
| 229 | # If the process crashed and is using FIFOs, like Chromium Android, the |
| 230 | # stdout and stderr pipes will be closed. |
| 231 | return |
| 232 | |
| 233 | out_fd = self._proc.stdout.fileno() |
| 234 | err_fd = self._proc.stderr.fileno() |
| 235 | select_fds = (out_fd, err_fd) |
| 236 | try: |
| 237 | read_fds, _, _ = select.select(select_fds, [], select_fds, max(deadline - time.time(), 0)) |
| 238 | except select.error, e: |
| 239 | # We can ignore EINVAL since it's likely the process just crashed and we'll |
| 240 | # figure that out the next time through the loop in _read(). |
| 241 | if e.args[0] == errno.EINVAL: |
| 242 | return |
| 243 | raise |
| 244 | |
| 245 | try: |
| 246 | # Note that we may get no data during read() even though |
| 247 | # select says we got something; see the select() man page |
| 248 | # on linux. I don't know if this happens on Mac OS and |
| 249 | # other Unixen as well, but we don't bother special-casing |
| 250 | # Linux because it's relatively harmless either way. |
| 251 | if out_fd in read_fds: |
| 252 | data = self._proc.stdout.read() |
| 253 | if not data and not stopping and (self._treat_no_data_as_crash or self._proc.poll()): |
| 254 | self._crashed = True |
| 255 | self._output += data |
| 256 | |
| 257 | if err_fd in read_fds: |
| 258 | data = self._proc.stderr.read() |
| 259 | if not data and not stopping and (self._treat_no_data_as_crash or self._proc.poll()): |
| 260 | self._crashed = True |
| 261 | self._error += data |
| 262 | except IOError, e: |
| 263 | # We can ignore the IOErrors because we will detect if the subporcess crashed |
| 264 | # the next time through the loop in _read() |
| 265 | pass |
| 266 | |
| 267 | def _wait_for_data_and_update_buffers_using_win32_apis(self, deadline): |
| 268 | # See http://code.activestate.com/recipes/440554-module-to-allow-asynchronous-subprocess-use-on-win/ |
| 269 | # and http://docs.activestate.com/activepython/2.6/pywin32/modules.html |
| 270 | # for documentation on all of these win32-specific modules. |
| 271 | now = time.time() |
| 272 | out_fh = msvcrt.get_osfhandle(self._proc.stdout.fileno()) |
| 273 | err_fh = msvcrt.get_osfhandle(self._proc.stderr.fileno()) |
| 274 | while (self._proc.poll() is None) and (now < deadline): |
| 275 | output = self._non_blocking_read_win32(out_fh) |
| 276 | error = self._non_blocking_read_win32(err_fh) |
| 277 | if output or error: |
| 278 | if output: |
| 279 | self._output += output |
| 280 | if error: |
| 281 | self._error += error |
| 282 | return |
| 283 | time.sleep(0.01) |
| 284 | now = time.time() |
| 285 | return |
| 286 | |
| 287 | def _non_blocking_read_win32(self, handle): |
| 288 | try: |
| 289 | _, avail, _ = win32pipe.PeekNamedPipe(handle, 0) |
| 290 | if avail > 0: |
| 291 | _, buf = win32file.ReadFile(handle, avail, None) |
| 292 | return buf |
| 293 | except Exception, e: |
| 294 | if e[0] not in (109, errno.ESHUTDOWN): # 109 == win32 ERROR_BROKEN_PIPE |
| 295 | raise |
| 296 | return None |
| 297 | |
| 298 | def has_crashed(self): |
| 299 | if not self._crashed and self.poll(): |
| 300 | self._crashed = True |
| 301 | self._handle_possible_interrupt() |
| 302 | return self._crashed |
| 303 | |
| 304 | # This read function is a bit oddly-designed, as it polls both stdout and stderr, yet |
| 305 | # only reads/returns from one of them (buffering both in local self._output/self._error). |
| 306 | # It might be cleaner to pass in the file descriptor to poll instead. |
| 307 | def _read(self, deadline, fetch_bytes_from_buffers_callback): |
| 308 | while True: |
| 309 | if self.has_crashed(): |
| 310 | return None |
| 311 | |
| 312 | if time.time() > deadline: |
| 313 | self._handle_timeout() |
| 314 | return None |
| 315 | |
| 316 | bytes = fetch_bytes_from_buffers_callback() |
| 317 | if bytes is not None: |
| 318 | return bytes |
| 319 | |
| 320 | if self._use_win32_apis: |
| 321 | self._wait_for_data_and_update_buffers_using_win32_apis(deadline) |
| 322 | else: |
| 323 | self._wait_for_data_and_update_buffers_using_select(deadline) |
| 324 | |
| 325 | def start(self): |
| 326 | if not self._proc: |
| 327 | self._start() |
| 328 | |
| 329 | def stop(self, timeout_secs=3.0): |
| 330 | if not self._proc: |
| 331 | return (None, None) |
| 332 | |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 333 | now = time.time() |
| 334 | if self._proc.stdin: |
| 335 | self._proc.stdin.close() |
| 336 | self._proc.stdin = None |
| 337 | killed = False |
| 338 | if timeout_secs: |
| 339 | deadline = now + timeout_secs |
| 340 | while self._proc.poll() is None and time.time() < deadline: |
| 341 | time.sleep(0.01) |
| 342 | if self._proc.poll() is None: |
| 343 | _log.warning('stopping %s(pid %d) timed out, killing it' % (self._name, self._proc.pid)) |
| 344 | |
| 345 | if self._proc.poll() is None: |
| 346 | self._kill() |
| 347 | killed = True |
| 348 | _log.debug('killed pid %d' % self._proc.pid) |
| 349 | |
| 350 | # read any remaining data on the pipes and return it. |
| 351 | if not killed: |
| 352 | if self._use_win32_apis: |
| 353 | self._wait_for_data_and_update_buffers_using_win32_apis(now) |
| 354 | else: |
| 355 | self._wait_for_data_and_update_buffers_using_select(now, stopping=True) |
| 356 | out, err = self._output, self._error |
| 357 | self._reset() |
| 358 | return (out, err) |
| 359 | |
| 360 | def kill(self): |
| 361 | self.stop(0.0) |
| 362 | |
| 363 | def _kill(self): |
| 364 | self._host.executive.kill_process(self._proc.pid) |
| 365 | if self._proc.poll() is not None: |
| 366 | self._proc.wait() |
| 367 | |
| 368 | def replace_outputs(self, stdout, stderr): |
| 369 | assert self._proc |
| 370 | if stdout: |
| 371 | self._proc.stdout.close() |
| 372 | self._proc.stdout = stdout |
| 373 | if stderr: |
| 374 | self._proc.stderr.close() |
| 375 | self._proc.stderr = stderr |