Home | History | Annotate | Download | only in servers
      1 # Copyright (C) 2011 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 name of Google Inc. 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 """Base class used to start servers used by the layout tests."""
     30 
     31 import errno
     32 import logging
     33 import socket
     34 import tempfile
     35 import time
     36 
     37 
     38 _log = logging.getLogger(__name__)
     39 
     40 
     41 class ServerError(Exception):
     42     pass
     43 
     44 
     45 class ServerBase(object):
     46     """A skeleton class for starting and stopping servers used by the layout tests."""
     47 
     48     def __init__(self, port_obj, output_dir):
     49         self._port_obj = port_obj
     50         self._executive = port_obj._executive
     51         self._filesystem = port_obj._filesystem
     52         self._platform = port_obj.host.platform
     53         self._output_dir = output_dir
     54 
     55         # We need a non-checkout-dependent place to put lock files, etc. We
     56         # don't use the Python default on the Mac because it defaults to a
     57         # randomly-generated directory under /var/folders and no one would ever
     58         # look there.
     59         tmpdir = tempfile.gettempdir()
     60         if self._platform.is_mac():
     61             tmpdir = '/tmp'
     62 
     63         self._runtime_path = self._filesystem.join(tmpdir, "WebKit")
     64         self._filesystem.maybe_make_directory(self._runtime_path)
     65 
     66         # Subclasses must override these fields.
     67         self._name = '<virtual>'
     68         self._log_prefixes = tuple()
     69         self._mappings = {}
     70         self._pid_file = None
     71         self._start_cmd = None
     72 
     73         # Subclasses may override these fields.
     74         self._env = None
     75         self._stdout = self._executive.PIPE
     76         self._stderr = self._executive.PIPE
     77         self._process = None
     78         self._pid = None
     79         self._error_log_path = None
     80 
     81     def start(self):
     82         """Starts the server. It is an error to start an already started server.
     83 
     84         This method also stops any stale servers started by a previous instance."""
     85         assert not self._pid, '%s server is already running' % self._name
     86 
     87         # Stop any stale servers left over from previous instances.
     88         if self._filesystem.exists(self._pid_file):
     89             try:
     90                 self._pid = int(self._filesystem.read_text_file(self._pid_file))
     91                 _log.debug('stale %s pid file, pid %d' % (self._name, self._pid))
     92                 self._stop_running_server()
     93             except (ValueError, UnicodeDecodeError):
     94                 # These could be raised if the pid file is corrupt.
     95                 self._remove_pid_file()
     96             self._pid = None
     97 
     98         self._remove_stale_logs()
     99         self._prepare_config()
    100         self._check_that_all_ports_are_available()
    101 
    102         self._pid = self._spawn_process()
    103 
    104         if self._wait_for_action(self._is_server_running_on_all_ports):
    105             _log.debug("%s successfully started (pid = %d)" % (self._name, self._pid))
    106         else:
    107             self._log_errors_from_subprocess()
    108             self._stop_running_server()
    109             raise ServerError('Failed to start %s server' % self._name)
    110 
    111     def stop(self):
    112         """Stops the server. Stopping a server that isn't started is harmless."""
    113         actual_pid = None
    114         try:
    115             if self._filesystem.exists(self._pid_file):
    116                 try:
    117                     actual_pid = int(self._filesystem.read_text_file(self._pid_file))
    118                 except (ValueError, UnicodeDecodeError):
    119                     # These could be raised if the pid file is corrupt.
    120                     pass
    121                 if not self._pid:
    122                     self._pid = actual_pid
    123 
    124             if not self._pid:
    125                 return
    126 
    127             if not actual_pid:
    128                 _log.warning('Failed to stop %s: pid file is missing' % self._name)
    129                 return
    130             if self._pid != actual_pid:
    131                 _log.warning('Failed to stop %s: pid file contains %d, not %d' %
    132                             (self._name, actual_pid, self._pid))
    133                 # Try to kill the existing pid, anyway, in case it got orphaned.
    134                 self._executive.kill_process(self._pid)
    135                 self._pid = None
    136                 return
    137 
    138             _log.debug("Attempting to shut down %s server at pid %d" % (self._name, self._pid))
    139             self._stop_running_server()
    140             _log.debug("%s server at pid %d stopped" % (self._name, self._pid))
    141             self._pid = None
    142         finally:
    143             # Make sure we delete the pid file no matter what happens.
    144             self._remove_pid_file()
    145 
    146     def _prepare_config(self):
    147         """This routine can be overridden by subclasses to do any sort
    148         of initialization required prior to starting the server that may fail."""
    149         pass
    150 
    151     def _remove_stale_logs(self):
    152         """This routine can be overridden by subclasses to try and remove logs
    153         left over from a prior run. This routine should log warnings if the
    154         files cannot be deleted, but should not fail unless failure to
    155         delete the logs will actually cause start() to fail."""
    156         # Sometimes logs are open in other processes but they should clear eventually.
    157         for log_prefix in self._log_prefixes:
    158             try:
    159                 self._remove_log_files(self._output_dir, log_prefix)
    160             except OSError, e:
    161                 _log.warning('Failed to remove old %s %s files' % (self._name, log_prefix))
    162 
    163     def _spawn_process(self):
    164         _log.debug('Starting %s server, cmd="%s"' % (self._name, self._start_cmd))
    165         process = self._executive.popen(self._start_cmd, env=self._env, stdout=self._stdout, stderr=self._stderr)
    166         pid = process.pid
    167         self._filesystem.write_text_file(self._pid_file, str(pid))
    168         return pid
    169 
    170     def _stop_running_server(self):
    171         self._wait_for_action(self._check_and_kill)
    172         if self._filesystem.exists(self._pid_file):
    173             self._filesystem.remove(self._pid_file)
    174 
    175     def _check_and_kill(self):
    176         if self._executive.check_running_pid(self._pid):
    177             _log.debug('pid %d is running, killing it' % self._pid)
    178             host = self._port_obj.host
    179             self._executive.kill_process(self._pid)
    180             return False
    181         else:
    182             _log.debug('pid %d is not running' % self._pid)
    183 
    184         return True
    185 
    186     def _remove_pid_file(self):
    187         if self._filesystem.exists(self._pid_file):
    188             self._filesystem.remove(self._pid_file)
    189 
    190     def _remove_log_files(self, folder, starts_with):
    191         files = self._filesystem.listdir(folder)
    192         for file in files:
    193             if file.startswith(starts_with):
    194                 full_path = self._filesystem.join(folder, file)
    195                 self._filesystem.remove(full_path)
    196 
    197     def _log_errors_from_subprocess(self):
    198         _log.error('logging %s errors, if any' % self._name)
    199         if self._process:
    200             _log.error('%s returncode %s' % (self._name, str(self._process.returncode)))
    201             if self._process.stderr:
    202                 stderr_text = self._process.stderr.read()
    203                 if stderr_text:
    204                     _log.error('%s stderr:' % self._name)
    205                     for line in stderr_text.splitlines():
    206                         _log.error('  %s' % line)
    207                 else:
    208                     _log.error('%s no stderr' % self._name)
    209             else:
    210                 _log.error('%s no stderr handle' % self._name)
    211         else:
    212             _log.error('%s no process' % self._name)
    213         if self._error_log_path and self._filesystem.exists(self._error_log_path):
    214             error_log_text = self._filesystem.read_text_file(self._error_log_path)
    215             if error_log_text:
    216                 _log.error('%s error log (%s) contents:' % (self._name, self._error_log_path))
    217                 for line in error_log_text.splitlines():
    218                     _log.error('  %s' % line)
    219             else:
    220                 _log.error('%s error log empty' % self._name)
    221             _log.error('')
    222         else:
    223             _log.error('%s no error log' % self._name)
    224 
    225     def _wait_for_action(self, action, wait_secs=20.0, sleep_secs=1.0):
    226         """Repeat the action for wait_sec or until it succeeds, sleeping for sleep_secs
    227         in between each attempt. Returns whether it succeeded."""
    228         start_time = time.time()
    229         while time.time() - start_time < wait_secs:
    230             if action():
    231                 return True
    232             _log.debug("Waiting for action: %s" % action)
    233             time.sleep(sleep_secs)
    234 
    235         return False
    236 
    237     def _is_server_running_on_all_ports(self):
    238         """Returns whether the server is running on all the desired ports."""
    239 
    240         # TODO(dpranke): crbug/378444 maybe pid is unreliable on win?
    241         if not self._platform.is_win() and not self._executive.check_running_pid(self._pid):
    242             _log.debug("Server isn't running at all")
    243             self._log_errors_from_subprocess()
    244             raise ServerError("Server exited")
    245 
    246         for mapping in self._mappings:
    247             s = socket.socket()
    248             port = mapping['port']
    249             try:
    250                 s.connect(('localhost', port))
    251                 _log.debug("Server running on %d" % port)
    252             except IOError, e:
    253                 if e.errno not in (errno.ECONNREFUSED, errno.ECONNRESET):
    254                     raise
    255                 _log.debug("Server NOT running on %d: %s" % (port, e))
    256                 return False
    257             finally:
    258                 s.close()
    259         return True
    260 
    261     def _check_that_all_ports_are_available(self):
    262         for mapping in self._mappings:
    263             s = socket.socket()
    264             if not self._platform.is_win():
    265                 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    266             port = mapping['port']
    267             try:
    268                 s.bind(('localhost', port))
    269             except IOError, e:
    270                 if e.errno in (errno.EALREADY, errno.EADDRINUSE):
    271                     raise ServerError('Port %d is already in use.' % port)
    272                 elif self._platform.is_win() and e.errno in (errno.WSAEACCES,):  # pylint: disable=E1101
    273                     raise ServerError('Port %d is already in use.' % port)
    274                 else:
    275                     raise
    276             finally:
    277                 s.close()
    278         _log.debug('all ports are available')
    279