blob: 5242e725fc550c191949fd61729b307745ae833d [file] [log] [blame]
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module containing methods and classes to interact with a devserver instance.
"""
import logging
import multiprocessing
import os
import re
import sys
import tempfile
import time
import urllib2
import constants
from chromite.lib import cros_build_lib
# Wait up to 15 minutes for the dev server to start. It can take a while to
# start when generating payloads in parallel.
DEV_SERVER_TIMEOUT = 900
CHECK_HEALTH_URL = 'http://127.0.0.1:8080/check_health'
KILL_TIMEOUT = 10
def GetIPAddress(device='eth0'):
"""Returns the IP Address for a given device using ifconfig.
socket.gethostname() is insufficient for machines where the host files are
not set up "correctly." Since some of our builders may have this issue,
this method gives you a generic way to get the address so you are reachable
either via a VM or remote machine on the same network.
"""
result = cros_build_lib.RunCommandCaptureOutput(
['/sbin/ifconfig', device], print_cmd=False)
match = re.search('.*inet addr:(\d+\.\d+\.\d+\.\d+).*', result.output)
if match:
return match.group(1)
cros_build_lib.Warning('Failed to find ip address in %r', result.output)
return None
def GenerateUpdateId(target, src, key, for_vm):
"""Returns a simple representation id of target and src paths."""
update_id = target
if src: update_id = '->'.join([src, update_id])
if key: update_id = '+'.join([update_id, key])
if not for_vm: update_id = '+'.join([update_id, 'patched_kernel'])
return update_id
class DevServerException(Exception):
"""Thrown when the devserver fails to start up correctly."""
class DevServerWrapper(multiprocessing.Process):
"""A Simple wrapper around a dev server instance."""
def __init__(self, test_root=None):
super(DevServerWrapper, self).__init__()
self.test_root = test_root or DevServerWrapper.MkChrootTemp(is_dir=True)
self._log_filename = os.path.join(self.test_root, 'dev_server.log')
self._pid_file = DevServerWrapper.MkChrootTemp(is_dir=False)
self._pid = None
# Chroot helper methods. Since we call a script into the chroot with paths
# created outside the chroot, translation back and forth is needed. These
# methods all assume the default chroot dir.
@staticmethod
def MkChrootTemp(is_dir):
"""Returns a temp(dir if is_dir, file otherwise) in the chroot."""
chroot_tmp = os.path.join(constants.SOURCE_ROOT, 'chroot', 'tmp')
if is_dir:
return tempfile.mkdtemp(prefix='devserver_wrapper', dir=chroot_tmp)
else:
return tempfile.NamedTemporaryFile(prefix='devserver_wrapper',
dir=chroot_tmp, delete=False).name
@staticmethod
def ToChrootPath(path):
"""Converts the path outside the chroot to one that can be used inside."""
path = os.path.abspath(path)
if '/chroot/' not in path:
raise ValueError('Path %s is not a path that points inside the chroot' %
path)
return '/' + path.partition('/chroot/')[2]
def _WaitUntilStarted(self):
"""Wait until the devserver has started."""
current_time = time.time()
deadline = current_time + DEV_SERVER_TIMEOUT
while current_time <= deadline:
try:
if not self.is_alive():
raise DevServerException('Devserver crashed while starting')
urllib2.urlopen(CHECK_HEALTH_URL, timeout=0.05)
return
except IOError:
# urlopen errors will throw a subclass of IOError if we can't connect.
pass
finally:
# Let's not churn needlessly in this loop as the devserver starts up.
time.sleep(1)
current_time = time.time()
else:
self.terminate()
raise DevServerException('Devserver did not start')
def run(self):
"""Kicks off devserver in a separate process and waits for it to finish."""
cmd = ['start_devserver',
'--pidfile', DevServerWrapper.ToChrootPath(self._pid_file),
'--logfile', DevServerWrapper.ToChrootPath(self._log_filename)]
return_obj = cros_build_lib.SudoRunCommand(
cmd, enter_chroot=True, debug_level=logging.DEBUG,
cwd=constants.SOURCE_ROOT, error_code_ok=True,
redirect_stdout=True, combine_stdout_stderr=True)
if return_obj.returncode != 0:
logging.error('Devserver terminated unexpectedly!')
logging.error(return_obj.output)
def Start(self):
"""Starts a background devserver and waits for it to start.
Starts a background devserver and waits for it to start. Will only return
once devserver has started and running pid has been read.
"""
self.start()
self._WaitUntilStarted()
# Pid file was passed into the chroot.
with open(self._pid_file, 'r') as f:
self._pid = f.read()
def Stop(self):
"""Kills the devserver instance with SIGTERM and SIGKILL if SIGTERM fails"""
if not self._pid:
logging.error('No devserver running.')
return
logging.debug('Stopping devserver instance with pid %s', self._pid)
if self.is_alive():
cros_build_lib.SudoRunCommand(['kill', self._pid],
debug_level=logging.DEBUG)
else:
logging.error('Devserver not running!')
return
self.join(KILL_TIMEOUT)
if self.is_alive():
logging.warning('Devserver is unstoppable. Killing with SIGKILL')
cros_build_lib.SudoRunCommand(['kill', '-9', self._pid],
debug_level=logging.DEBUG)
def PrintLog(self):
"""Print devserver output to stdout."""
print '--- Start output from %s ---' % self._log_filename
# Open in update mode in case the child process hasn't opened the file yet.
with open(self._log_filename) as log:
sys.stdout.writelines(log)
print '--- End output from %s ---' % self._log_filename
@classmethod
def GetDevServerURL(cls, port=None, sub_dir=None):
"""Returns the dev server url for a given port and sub directory."""
if not port: port = 8080
url = 'http://%(ip)s:%(port)s' % {'ip': GetIPAddress(), 'port': str(port)}
if sub_dir:
url += '/' + sub_dir
return url
@classmethod
def WipePayloadCache(cls):
"""Cleans up devserver cache of payloads."""
cros_build_lib.Info('Cleaning up previously generated payloads.')
cmd = ['start_devserver', '--clear_cache', '--exit']
cros_build_lib.SudoRunCommand(
cmd, enter_chroot=True, print_cmd=False, combine_stdout_stderr=True,
redirect_stdout=True, redirect_stderr=True, cwd=constants.SOURCE_ROOT)