Home | History | Annotate | Download | only in port
      1 #!/usr/bin/env python
      2 # Copyright (C) 2010 Google Inc. All rights reserved.
      3 #
      4 # Redistribution and use in source and binary forms, with or without
      5 # modification, are permitted provided that the following conditions are
      6 # met:
      7 #
      8 #     * Redistributions of source code must retain the above copyright
      9 # notice, this list of conditions and the following disclaimer.
     10 #     * Redistributions in binary form must reproduce the above
     11 # copyright notice, this list of conditions and the following disclaimer
     12 # in the documentation and/or other materials provided with the
     13 # distribution.
     14 #     * Neither the Google name nor the names of its
     15 # contributors may be used to endorse or promote products derived from
     16 # this software without specific prior written permission.
     17 #
     18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29 
     30 """Package that implements the ServerProcess wrapper class"""
     31 
     32 import logging
     33 import os
     34 import select
     35 import signal
     36 import subprocess
     37 import sys
     38 import time
     39 if sys.platform != 'win32':
     40     import fcntl
     41 
     42 from webkitpy.common.system.executive import Executive
     43 
     44 _log = logging.getLogger("webkitpy.layout_tests.port.server_process")
     45 
     46 
     47 class ServerProcess:
     48     """This class provides a wrapper around a subprocess that
     49     implements a simple request/response usage model. The primary benefit
     50     is that reading responses takes a timeout, so that we don't ever block
     51     indefinitely. The class also handles transparently restarting processes
     52     as necessary to keep issuing commands."""
     53 
     54     def __init__(self, port_obj, name, cmd, env=None, executive=Executive()):
     55         self._port = port_obj
     56         self._name = name
     57         self._cmd = cmd
     58         self._env = env
     59         self._reset()
     60         self._executive = executive
     61 
     62     def _reset(self):
     63         self._proc = None
     64         self._output = ''
     65         self.crashed = False
     66         self.timed_out = False
     67         self.error = ''
     68 
     69     def _start(self):
     70         if self._proc:
     71             raise ValueError("%s already running" % self._name)
     72         self._reset()
     73         # close_fds is a workaround for http://bugs.python.org/issue2320
     74         close_fds = sys.platform not in ('win32', 'cygwin')
     75         self._proc = subprocess.Popen(self._cmd, stdin=subprocess.PIPE,
     76                                       stdout=subprocess.PIPE,
     77                                       stderr=subprocess.PIPE,
     78                                       close_fds=close_fds,
     79                                       env=self._env)
     80         fd = self._proc.stdout.fileno()
     81         fl = fcntl.fcntl(fd, fcntl.F_GETFL)
     82         fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
     83         fd = self._proc.stderr.fileno()
     84         fl = fcntl.fcntl(fd, fcntl.F_GETFL)
     85         fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
     86 
     87     def handle_interrupt(self):
     88         """This routine checks to see if the process crashed or exited
     89         because of a keyboard interrupt and raises KeyboardInterrupt
     90         accordingly."""
     91         if self.crashed:
     92             # This is hex code 0xc000001d, which is used for abrupt
     93             # termination. This happens if we hit ctrl+c from the prompt
     94             # and we happen to be waiting on the DumpRenderTree.
     95             # sdoyon: Not sure for which OS and in what circumstances the
     96             # above code is valid. What works for me under Linux to detect
     97             # ctrl+c is for the subprocess returncode to be negative
     98             # SIGINT. And that agrees with the subprocess documentation.
     99             if (-1073741510 == self._proc.returncode or
    100                 - signal.SIGINT == self._proc.returncode):
    101                 raise KeyboardInterrupt
    102             return
    103 
    104     def poll(self):
    105         """Check to see if the underlying process is running; returns None
    106         if it still is (wrapper around subprocess.poll)."""
    107         if self._proc:
    108             # poll() is not threadsafe and can throw OSError due to:
    109             # http://bugs.python.org/issue1731717
    110             return self._proc.poll()
    111         return None
    112 
    113     def write(self, input):
    114         """Write a request to the subprocess. The subprocess is (re-)start()'ed
    115         if is not already running."""
    116         if not self._proc:
    117             self._start()
    118         try:
    119             self._proc.stdin.write(input)
    120         except IOError, e:
    121             self.stop()
    122             self.crashed = True
    123 
    124     def read_line(self, timeout):
    125         """Read a single line from the subprocess, waiting until the deadline.
    126         If the deadline passes, the call times out. Note that even if the
    127         subprocess has crashed or the deadline has passed, if there is output
    128         pending, it will be returned.
    129 
    130         Args:
    131             timeout: floating-point number of seconds the call is allowed
    132                 to block for. A zero or negative number will attempt to read
    133                 any existing data, but will not block. There is no way to
    134                 block indefinitely.
    135         Returns:
    136             output: data returned, if any. If no data is available and the
    137                 call times out or crashes, an empty string is returned. Note
    138                 that the returned string includes the newline ('\n')."""
    139         return self._read(timeout, size=0)
    140 
    141     def read(self, timeout, size):
    142         """Attempts to read size characters from the subprocess, waiting until
    143         the deadline passes. If the deadline passes, any available data will be
    144         returned. Note that even if the deadline has passed or if the
    145         subprocess has crashed, any available data will still be returned.
    146 
    147         Args:
    148             timeout: floating-point number of seconds the call is allowed
    149                 to block for. A zero or negative number will attempt to read
    150                 any existing data, but will not block. There is no way to
    151                 block indefinitely.
    152             size: amount of data to read. Must be a postive integer.
    153         Returns:
    154             output: data returned, if any. If no data is available, an empty
    155                 string is returned.
    156         """
    157         if size <= 0:
    158             raise ValueError('ServerProcess.read() called with a '
    159                              'non-positive size: %d ' % size)
    160         return self._read(timeout, size)
    161 
    162     def _read(self, timeout, size):
    163         """Internal routine that actually does the read."""
    164         index = -1
    165         out_fd = self._proc.stdout.fileno()
    166         err_fd = self._proc.stderr.fileno()
    167         select_fds = (out_fd, err_fd)
    168         deadline = time.time() + timeout
    169         while not self.timed_out and not self.crashed:
    170             # poll() is not threadsafe and can throw OSError due to:
    171             # http://bugs.python.org/issue1731717
    172             if self._proc.poll() != None:
    173                 self.crashed = True
    174                 self.handle_interrupt()
    175 
    176             now = time.time()
    177             if now > deadline:
    178                 self.timed_out = True
    179 
    180             # Check to see if we have any output we can return.
    181             if size and len(self._output) >= size:
    182                 index = size
    183             elif size == 0:
    184                 index = self._output.find('\n') + 1
    185 
    186             if index > 0 or self.crashed or self.timed_out:
    187                 output = self._output[0:index]
    188                 self._output = self._output[index:]
    189                 return output
    190 
    191             # Nope - wait for more data.
    192             (read_fds, write_fds, err_fds) = select.select(select_fds, [],
    193                                                            select_fds,
    194                                                            deadline - now)
    195             try:
    196                 if out_fd in read_fds:
    197                     self._output += self._proc.stdout.read()
    198                 if err_fd in read_fds:
    199                     self.error += self._proc.stderr.read()
    200             except IOError, e:
    201                 pass
    202 
    203     def stop(self):
    204         """Stop (shut down) the subprocess), if it is running."""
    205         pid = self._proc.pid
    206         self._proc.stdin.close()
    207         self._proc.stdout.close()
    208         if self._proc.stderr:
    209             self._proc.stderr.close()
    210         if sys.platform not in ('win32', 'cygwin'):
    211             # Closing stdin/stdout/stderr hangs sometimes on OS X,
    212             # (see restart(), above), and anyway we don't want to hang
    213             # the harness if DumpRenderTree is buggy, so we wait a couple
    214             # seconds to give DumpRenderTree a chance to clean up, but then
    215             # force-kill the process if necessary.
    216             KILL_TIMEOUT = 3.0
    217             timeout = time.time() + KILL_TIMEOUT
    218             # poll() is not threadsafe and can throw OSError due to:
    219             # http://bugs.python.org/issue1731717
    220             while self._proc.poll() is None and time.time() < timeout:
    221                 time.sleep(0.1)
    222             # poll() is not threadsafe and can throw OSError due to:
    223             # http://bugs.python.org/issue1731717
    224             if self._proc.poll() is None:
    225                 _log.warning('stopping %s timed out, killing it' %
    226                              self._name)
    227                 self._executive.kill_process(self._proc.pid)
    228                 _log.warning('killed')
    229         self._reset()
    230