blob: 23fc98591d5cfc58eb69990aa6c47f580ff414af [file] [log] [blame]
# 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 web 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 web 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')