| # Copyright (C) 2011 Google Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # * Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # * Redistributions in binary form must reproduce the above |
| # copyright notice, this list of conditions and the following disclaimer |
| # in the documentation and/or other materials provided with the |
| # distribution. |
| # * Neither the name of Google Inc. nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| """Base class used to start servers used by the layout tests.""" |
| |
| import errno |
| import logging |
| import socket |
| import tempfile |
| |
| |
| _log = logging.getLogger(__name__) |
| |
| |
| class ServerError(Exception): |
| pass |
| |
| |
| class ServerBase(object): |
| """A skeleton class for starting and stopping servers used by the layout tests.""" |
| |
| def __init__(self, port_obj, output_dir): |
| self._port_obj = port_obj |
| self._executive = port_obj.host.executive |
| self._filesystem = port_obj.host.filesystem |
| self._platform = port_obj.host.platform |
| self._output_dir = output_dir |
| |
| # On Mac and Linux tmpdir is set to '/tmp' for (i) consistency |
| # and (ii) because it is hardcoded in the Apache |
| # ScoreBoardFile directive. |
| tmpdir = tempfile.gettempdir() |
| if self._platform.is_mac() or self._platform.is_linux(): |
| tmpdir = '/tmp' |
| |
| self._runtime_path = self._filesystem.join(tmpdir, 'WebKit') |
| self._filesystem.maybe_make_directory(self._runtime_path) |
| |
| # Subclasses must override these fields. |
| self._name = '<virtual>' |
| self._log_prefixes = tuple() |
| self._mappings = {} |
| self._pid_file = None |
| self._start_cmd = None |
| |
| # Subclasses may override these fields. |
| self._env = None |
| self._cwd = None |
| self._stdout = self._executive.PIPE |
| self._stderr = self._executive.PIPE |
| self._process = None |
| self._pid = None |
| self._error_log_path = None |
| |
| def start(self): |
| """Starts the server. It is an error to start an already started server. |
| |
| This method also stops any stale servers started by a previous instance. |
| """ |
| assert not self._pid, '%s server is already running' % self._name |
| |
| # Stop any stale servers left over from previous instances. |
| if self._filesystem.exists(self._pid_file): |
| try: |
| self._pid = int(self._filesystem.read_text_file(self._pid_file)) |
| _log.debug('stale %s pid file, pid %d', self._name, self._pid) |
| self._stop_running_server() |
| except (ValueError, UnicodeDecodeError): |
| # These could be raised if the pid file is corrupt. |
| self._remove_pid_file() |
| self._pid = None |
| |
| self._remove_stale_logs() |
| self._prepare_config() |
| self._check_that_all_ports_are_available() |
| |
| self._pid = self._spawn_process() |
| |
| if self._wait_for_action(self._is_server_running_on_all_ports): |
| _log.debug('%s successfully started (pid = %d)', self._name, self._pid) |
| else: |
| self._log_errors_from_subprocess() |
| self._stop_running_server() |
| raise ServerError('Failed to start %s server' % self._name) |
| |
| def stop(self): |
| """Stops the server. Stopping a server that isn't started is harmless.""" |
| actual_pid = None |
| try: |
| if self._filesystem.exists(self._pid_file): |
| try: |
| actual_pid = int(self._filesystem.read_text_file(self._pid_file)) |
| except (ValueError, UnicodeDecodeError): |
| # These could be raised if the pid file is corrupt. |
| pass |
| if not self._pid: |
| self._pid = actual_pid |
| |
| if not self._pid: |
| return |
| |
| if not actual_pid: |
| _log.warning('Failed to stop %s: pid file is missing', self._name) |
| return |
| if self._pid != actual_pid: |
| _log.warning('Failed to stop %s: pid file contains %d, not %d', |
| self._name, actual_pid, self._pid) |
| # Try to kill the existing pid, anyway, in case it got orphaned. |
| self._executive.kill_process(self._pid) |
| self._pid = None |
| return |
| |
| _log.debug('Attempting to shut down %s server at pid %d', self._name, self._pid) |
| self._stop_running_server() |
| _log.debug('%s server at pid %d stopped', self._name, self._pid) |
| self._pid = None |
| finally: |
| # Make sure we delete the pid file no matter what happens. |
| self._remove_pid_file() |
| |
| def _prepare_config(self): |
| """This routine can be overridden by subclasses to do any sort |
| of initialization required prior to starting the server that may fail. |
| """ |
| |
| def _remove_stale_logs(self): |
| """This routine can be overridden by subclasses to try and remove logs |
| left over from a prior run. This routine should log warnings if the |
| files cannot be deleted, but should not fail unless failure to |
| delete the logs will actually cause start() to fail. |
| """ |
| # Sometimes logs are open in other processes but they should clear eventually. |
| for log_prefix in self._log_prefixes: |
| try: |
| self._remove_log_files(self._output_dir, log_prefix) |
| except OSError: |
| _log.warning('Failed to remove old %s %s files', self._name, log_prefix) |
| |
| def _spawn_process(self): |
| _log.debug('Starting %s server, cmd="%s"', self._name, self._start_cmd) |
| self._process = self._executive.popen(self._start_cmd, |
| env=self._env, |
| cwd=self._cwd, |
| stdout=self._stdout, |
| stderr=self._stderr) |
| pid = self._process.pid |
| self._filesystem.write_text_file(self._pid_file, str(pid)) |
| return pid |
| |
| def _stop_running_server(self): |
| self._wait_for_action(self._check_and_kill) |
| if self._filesystem.exists(self._pid_file): |
| self._filesystem.remove(self._pid_file) |
| |
| def _check_and_kill(self): |
| if self._executive.check_running_pid(self._pid): |
| _log.debug('pid %d is running, killing it', self._pid) |
| self._executive.kill_process(self._pid) |
| return False |
| else: |
| _log.debug('pid %d is not running', self._pid) |
| |
| return True |
| |
| def _remove_pid_file(self): |
| if self._filesystem.exists(self._pid_file): |
| self._filesystem.remove(self._pid_file) |
| |
| def _remove_log_files(self, folder, starts_with): |
| files = self._filesystem.listdir(folder) |
| for file in files: |
| if file.startswith(starts_with): |
| full_path = self._filesystem.join(folder, file) |
| self._filesystem.remove(full_path) |
| |
| def _log_errors_from_subprocess(self): |
| _log.error('logging %s errors, if any', self._name) |
| if self._process: |
| _log.error('%s returncode %s', self._name, str(self._process.returncode)) |
| if self._process.stderr: |
| stderr_text = self._process.stderr.read() |
| if stderr_text: |
| _log.error('%s stderr:', self._name) |
| for line in stderr_text.splitlines(): |
| _log.error(' %s', line) |
| else: |
| _log.error('%s no stderr', self._name) |
| else: |
| _log.error('%s no stderr handle', self._name) |
| else: |
| _log.error('%s no process', self._name) |
| if self._error_log_path and self._filesystem.exists(self._error_log_path): |
| error_log_text = self._filesystem.read_text_file(self._error_log_path) |
| if error_log_text: |
| _log.error('%s error log (%s) contents:', self._name, self._error_log_path) |
| for line in error_log_text.splitlines(): |
| _log.error(' %s', line) |
| else: |
| _log.error('%s error log empty', self._name) |
| _log.error('') |
| else: |
| _log.error('%s no error log', self._name) |
| |
| def _wait_for_action(self, action, wait_secs=20.0, sleep_secs=1.0): |
| """Repeat the action for wait_sec or until it succeeds, sleeping for sleep_secs |
| in between each attempt. Returns whether it succeeded. |
| """ |
| start_time = self._port_obj.host.time() |
| while self._port_obj.host.time() - start_time < wait_secs: |
| if action(): |
| return True |
| _log.debug('Waiting for action: %s', action) |
| self._port_obj.host.sleep(sleep_secs) |
| |
| return False |
| |
| def _is_server_running_on_all_ports(self): |
| """Returns whether the server is running on all the desired ports.""" |
| |
| # TODO(dpranke): crbug/378444 maybe pid is unreliable on win? |
| if not self._platform.is_win() and not self._executive.check_running_pid(self._pid): |
| _log.debug("Server isn't running at all") |
| self._log_errors_from_subprocess() |
| raise ServerError('Server exited') |
| |
| for mapping in self._mappings: |
| s = socket.socket() |
| port = mapping['port'] |
| try: |
| s.connect(('localhost', port)) |
| _log.debug('Server running on %d', port) |
| except IOError as error: |
| if error.errno not in (errno.ECONNREFUSED, errno.ECONNRESET): |
| raise |
| _log.debug('Server NOT running on %d: %s', port, error) |
| return False |
| finally: |
| s.close() |
| return True |
| |
| def _check_that_all_ports_are_available(self): |
| for mapping in self._mappings: |
| s = socket.socket() |
| if not self._platform.is_win(): |
| s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| port = mapping['port'] |
| try: |
| s.bind(('localhost', port)) |
| except IOError as error: |
| if error.errno in (errno.EALREADY, errno.EADDRINUSE): |
| raise ServerError('Port %d is already in use.' % port) |
| elif self._platform.is_win() and error.errno in (errno.WSAEACCES,): # pylint: disable=no-member |
| raise ServerError('Port %d is already in use.' % port) |
| else: |
| raise |
| finally: |
| s.close() |
| _log.debug('all ports are available') |