Commit 2bd7fe6b authored by Robert Ma's avatar Robert Ma Committed by Commit Bot

[blinkpy] Poll server for aliveness in cli_wrapper

This prevents the server from dying silently in run_blink_[SERVER].py
by checking whether the server is alive every second. (See the race
condition where the server dies after a seemingly successful start in
the linked issue.)

Unfortunately, this does not fix run_web_tests.py.

Bug: 1090491
Change-Id: Ifb5a73284716a980a752ce3ed9c8210ba9510f8e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2229472
Commit-Queue: Robert Ma <robertma@chromium.org>
Reviewed-by: default avatarStephen McGruer <smcgruer@chromium.org>
Cr-Commit-Position: refs/heads/master@{#775251}
parent 1772227e
...@@ -38,9 +38,10 @@ import signal ...@@ -38,9 +38,10 @@ import signal
from blinkpy.common.host import Host from blinkpy.common.host import Host
from blinkpy.common.system.log_utils import configure_logging from blinkpy.common.system.log_utils import configure_logging
from blinkpy.web_tests.port.base import ARTIFACTS_SUB_DIR
from blinkpy.web_tests.port.factory import configuration_options from blinkpy.web_tests.port.factory import configuration_options
from blinkpy.web_tests.port.factory import python_server_options from blinkpy.web_tests.port.factory import python_server_options
from blinkpy.web_tests.port.base import ARTIFACTS_SUB_DIR from blinkpy.web_tests.servers.server_base import ServerError
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
...@@ -64,8 +65,7 @@ def main(server_constructor, ...@@ -64,8 +65,7 @@ def main(server_constructor,
description=None, description=None,
**kwargs): **kwargs):
host = Host() host = Host()
# Signals will interrupt sleep, so we can use a long duration. sleep_fn = sleep_fn or (lambda: host.sleep(1))
sleep_fn = sleep_fn or (lambda: host.sleep(10))
parser = optparse.OptionParser( parser = optparse.OptionParser(
description=description, formatter=RawTextHelpFormatter()) description=description, formatter=RawTextHelpFormatter())
...@@ -106,6 +106,11 @@ def main(server_constructor, ...@@ -106,6 +106,11 @@ def main(server_constructor,
try: try:
while True: while True:
sleep_fn() sleep_fn()
if not server.alive():
raise ServerError('Server is no longer listening')
except ServerError as e:
_log.error(e)
except (SystemExit, KeyboardInterrupt): except (SystemExit, KeyboardInterrupt):
_log.info('Exiting...') _log.info('Exiting...')
finally:
server.stop() server.stop()
...@@ -13,6 +13,7 @@ class MockServer(object): ...@@ -13,6 +13,7 @@ class MockServer(object):
self.kwargs = kwargs self.kwargs = kwargs
self.start_called = False self.start_called = False
self.stop_called = False self.stop_called = False
self.is_alive = True
def start(self): def start(self):
self.start_called = True self.start_called = True
...@@ -20,12 +21,15 @@ class MockServer(object): ...@@ -20,12 +21,15 @@ class MockServer(object):
def stop(self): def stop(self):
self.stop_called = True self.stop_called = True
def alive(self):
return self.is_alive
class CliWrapperTest(unittest.TestCase): class CliWrapperTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.server = None self.server = None
def test_main(self): def test_main_success(self):
def mock_server_constructor(*args, **kwargs): def mock_server_constructor(*args, **kwargs):
self.server = MockServer(args, kwargs) self.server = MockServer(args, kwargs)
return self.server return self.server
...@@ -36,3 +40,17 @@ class CliWrapperTest(unittest.TestCase): ...@@ -36,3 +40,17 @@ class CliWrapperTest(unittest.TestCase):
cli_wrapper.main(mock_server_constructor, sleep_fn=raise_exit, argv=[]) cli_wrapper.main(mock_server_constructor, sleep_fn=raise_exit, argv=[])
self.assertTrue(self.server.start_called) self.assertTrue(self.server.start_called)
self.assertTrue(self.server.stop_called) self.assertTrue(self.server.stop_called)
def test_main_server_error_after_start(self):
def mock_server_constructor(*args, **kwargs):
self.server = MockServer(args, kwargs)
return self.server
def server_error():
self.server.is_alive = False
cli_wrapper.main(mock_server_constructor,
sleep_fn=server_error,
argv=[])
self.assertTrue(self.server.start_called)
self.assertTrue(self.server.stop_called)
...@@ -75,7 +75,11 @@ class ServerBase(object): ...@@ -75,7 +75,11 @@ class ServerBase(object):
# redirect them to files. # redirect them to files.
self._stdout = self._executive.PIPE self._stdout = self._executive.PIPE
self._stderr = self._executive.PIPE self._stderr = self._executive.PIPE
# The entrypoint process of the server, which may not be the daemon,
# e.g. apachectl.
self._process = None self._process = None
# The PID of the server daemon, which may be different from
# self._process.pid.
self._pid = None self._pid = None
self._error_log_path = None self._error_log_path = None
...@@ -150,6 +154,14 @@ class ServerBase(object): ...@@ -150,6 +154,14 @@ class ServerBase(object):
# Make sure we delete the pid file no matter what happens. # Make sure we delete the pid file no matter what happens.
self._remove_pid_file() self._remove_pid_file()
def alive(self):
"""Checks whether the server is alive."""
# This by default checks both the process and ports.
# At this point, we think the server has started up, so successes are
# normal while failures are not.
return self._is_server_running_on_all_ports(
success_log_level=logging.DEBUG, failure_log_level=logging.INFO)
def _prepare_config(self): def _prepare_config(self):
"""This routine can be overridden by subclasses to do any sort """This routine can be overridden by subclasses to do any sort
of initialization required prior to starting the server that may fail. of initialization required prior to starting the server that may fail.
...@@ -252,9 +264,17 @@ class ServerBase(object): ...@@ -252,9 +264,17 @@ class ServerBase(object):
return False return False
def _is_server_running_on_all_ports(self): def _is_server_running_on_all_ports(self,
"""Returns whether the server is running on all the desired ports.""" success_log_level=logging.INFO,
failure_log_level=logging.DEBUG):
"""Returns whether the server is running on all the desired ports.
Args:
success_log_level: Logging level for success (default: INFO)
failure_log_level: Logging level for failure (default: DEBUG)
"""
# Check self._pid instead of self._process because the latter might be a
# control process that exits after spawning up the daemon.
# TODO(dpranke): crbug/378444 maybe pid is unreliable on win? # TODO(dpranke): crbug/378444 maybe pid is unreliable on win?
if (not self._platform.is_win() if (not self._platform.is_win()
and not self._executive.check_running_pid(self._pid)): and not self._executive.check_running_pid(self._pid)):
...@@ -268,12 +288,14 @@ class ServerBase(object): ...@@ -268,12 +288,14 @@ class ServerBase(object):
scheme = mapping['scheme'] scheme = mapping['scheme']
try: try:
s.connect(('localhost', port)) s.connect(('localhost', port))
_log.info('Server running on %s://localhost:%d', scheme, port) _log.log(success_log_level,
'Server running on %s://localhost:%d', scheme, port)
except IOError as error: except IOError as error:
if error.errno not in (errno.ECONNREFUSED, errno.ECONNRESET): if error.errno not in (errno.ECONNREFUSED, errno.ECONNRESET):
raise raise
_log.debug('Server NOT running on %s://localhost:%d : %s', _log.log(failure_log_level,
scheme, port, error) 'Server NOT running on %s://localhost:%d : %s',
scheme, port, error)
return False return False
finally: finally:
s.close() s.close()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment