| # 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 with common routines between the Apache, Lighttpd, and websocket servers.""" |
| |
| import errno |
| import json |
| import logging |
| import socket |
| import sys |
| import tempfile |
| import time |
| |
| |
| _log = logging.getLogger(__name__) |
| |
| |
| class ServerError(Exception): |
| pass |
| |
| |
| class HttpServerBase(object): |
| """A skeleton class for starting and stopping servers used by the layout tests.""" |
| |
| HTTP_SERVER_PORT = 8000 |
| ALTERNATIVE_HTTP_SERVER_PORT = 8080 |
| HTTPS_SERVER_PORT = 8443 |
| |
| def __init__(self, port_obj): |
| self._executive = port_obj._executive |
| self._filesystem = port_obj._filesystem |
| self._name = '<virtual>' |
| self._mappings = {} |
| self._pid = None |
| self._pid_file = None |
| self._port_obj = port_obj |
| self.tests_dir = self._port_obj.layout_tests_dir() |
| |
| # We need a non-checkout-dependent place to put lock files, etc. We |
| # don't use the Python default on the Mac because it defaults to a |
| # randomly-generated directory under /var/folders and no one would ever |
| # look there. |
| tmpdir = tempfile.gettempdir() |
| if port_obj.host.platform.is_mac(): |
| tmpdir = '/tmp' |
| |
| self._runtime_path = self._filesystem.join(tmpdir, "WebKit") |
| self._filesystem.maybe_make_directory(self._runtime_path) |
| |
| def ports_to_forward(self): |
| return [mapping['port'] for mapping in self._mappings] |
| |
| 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)) |
| 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 sys.platform == 'cygwin': |
| # Starting the server takes longer time on Cygwin |
| server_started = self._wait_for_action(self._is_server_running_on_all_ports, 60) |
| else: |
| server_started = self._wait_for_action(self._is_server_running_on_all_ports) |
| |
| if server_started: |
| _log.debug("%s successfully started (pid = %d)" % (self._name, self._pid)) |
| else: |
| 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.""" |
| pass |
| |
| 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.""" |
| pass |
| |
| def _spawn_process(self): |
| """This routine must be implemented by subclasses to actually start the server. |
| |
| This routine returns the pid of the started process, and also ensures that that |
| pid has been written to self._pid_file.""" |
| raise NotImplementedError() |
| |
| def _stop_running_server(self): |
| """This routine must be implemented by subclasses to actually stop the running server listed in self._pid_file.""" |
| raise NotImplementedError() |
| |
| # Utility routines. |
| |
| def aliases(self): |
| """Return path pairs used to define aliases. First item is URL path and second |
| one is actual location in the file system.""" |
| json_data = self._filesystem.read_text_file(self._port_obj.path_from_webkit_base("Tools", "Scripts", "webkitpy", "layout_tests", "servers", "aliases.json")) |
| return self._build_alias_path_pairs(json.loads(json_data)) |
| |
| def _build_alias_path_pairs(self, data): |
| def _make_path(path): |
| return self._filesystem.join(self.tests_dir, self._filesystem.normpath(path)) |
| return [(alias, _make_path(path)) for (alias, path) in data] |
| |
| 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 _wait_for_action(self, action, wait_secs=20.0, sleep_secs=0.1): |
| """Repeat the action for wait_sec or until it succeeds, sleeping for sleep_secs |
| in between each attempt. Returns whether it succeeded.""" |
| start_time = time.time() |
| while time.time() - start_time < wait_secs: |
| if action(): |
| return True |
| _log.debug("Waiting for action: %s" % action) |
| time.sleep(sleep_secs) |
| |
| return False |
| |
| def _is_server_running_on_all_ports(self): |
| """Returns whether the server is running on all the desired ports.""" |
| if not self._port_obj.host.platform.is_win() and not self._executive.check_running_pid(self._pid): |
| _log.debug("Server isn't running at all") |
| raise ServerError("Server exited") |
| |
| for mapping in self._mappings: |
| if not self._is_running_on_port(mapping['port']): |
| return False |
| return True |
| |
| @classmethod |
| def _is_running_on_port(cls, port): |
| s = socket.socket() |
| try: |
| s.connect(('localhost', port)) |
| _log.debug("Server running on %d" % port) |
| except IOError as e: |
| if e.errno not in (errno.ECONNREFUSED, errno.ECONNRESET): |
| raise |
| _log.debug("Server NOT running on %d: %s" % (port, e)) |
| return False |
| finally: |
| s.close() |
| return True |
| |
| def _check_that_all_ports_are_available(self): |
| for mapping in self._mappings: |
| s = socket.socket() |
| s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| port = mapping['port'] |
| try: |
| s.bind(('localhost', port)) |
| except IOError as e: |
| if e.errno in (errno.EALREADY, errno.EADDRINUSE): |
| raise ServerError('Port %d is already in use.' % port) |
| elif sys.platform.startswith('win') and e.errno in (errno.WSAEACCES,): # pylint: disable=E1101 |
| raise ServerError('Port %d is already in use.' % port) |
| else: |
| raise |
| finally: |
| s.close() |
| |
| |
| def is_http_server_running(): |
| return HttpServerBase._is_running_on_port(HttpServerBase.HTTP_SERVER_PORT) |