Blame testing/talos/talos/talos_process.py

Packit f0b94e
# This Source Code Form is subject to the terms of the Mozilla Public
Packit f0b94e
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
Packit f0b94e
# You can obtain one at http://mozilla.org/MPL/2.0/.
Packit f0b94e
from __future__ import absolute_import
Packit f0b94e
Packit f0b94e
import pprint
Packit f0b94e
import signal
Packit f0b94e
import time
Packit f0b94e
import traceback
Packit f0b94e
import subprocess
Packit f0b94e
from threading import Event
Packit f0b94e
Packit f0b94e
import mozcrash
Packit f0b94e
import psutil
Packit f0b94e
from mozlog import get_proxy_logger
Packit f0b94e
from mozprocess import ProcessHandler
Packit f0b94e
from utils import TalosError
Packit f0b94e
Packit f0b94e
LOG = get_proxy_logger()
Packit f0b94e
Packit f0b94e
Packit f0b94e
class ProcessContext(object):
Packit f0b94e
    """
Packit f0b94e
    Store useful results of the browser execution.
Packit f0b94e
    """
Packit f0b94e
    def __init__(self):
Packit f0b94e
        self.output = None
Packit f0b94e
        self.process = None
Packit f0b94e
Packit f0b94e
    @property
Packit f0b94e
    def pid(self):
Packit f0b94e
        return self.process and self.process.pid
Packit f0b94e
Packit f0b94e
    def kill_process(self):
Packit f0b94e
        """
Packit f0b94e
        Kill the process, returning the exit code or None if the process
Packit f0b94e
        is already finished.
Packit f0b94e
        """
Packit f0b94e
        if self.process and self.process.is_running():
Packit f0b94e
            LOG.debug("Terminating %s" % self.process)
Packit f0b94e
            try:
Packit f0b94e
                self.process.terminate()
Packit f0b94e
            except psutil.NoSuchProcess:
Packit f0b94e
                procs = self.process.children()
Packit f0b94e
                for p in procs:
Packit f0b94e
                    c = ProcessContext()
Packit f0b94e
                    c.process = p
Packit f0b94e
                    c.kill_process()
Packit f0b94e
                return self.process.returncode
Packit f0b94e
            try:
Packit f0b94e
                return self.process.wait(3)
Packit f0b94e
            except psutil.TimeoutExpired:
Packit f0b94e
                self.process.kill()
Packit f0b94e
                # will raise TimeoutExpired if unable to kill
Packit f0b94e
                return self.process.wait(3)
Packit f0b94e
Packit f0b94e
Packit f0b94e
class Reader(object):
Packit f0b94e
    def __init__(self, event):
Packit f0b94e
        self.output = []
Packit f0b94e
        self.got_end_timestamp = False
Packit f0b94e
        self.got_timeout = False
Packit f0b94e
        self.timeout_message = ''
Packit f0b94e
        self.event = event
Packit f0b94e
        self.proc = None
Packit f0b94e
Packit f0b94e
    def __call__(self, line):
Packit f0b94e
        if line.find('__endTimestamp') != -1:
Packit f0b94e
            self.got_end_timestamp = True
Packit f0b94e
            self.event.set()
Packit f0b94e
        elif line == 'TART: TIMEOUT':
Packit f0b94e
            self.got_timeout = True
Packit f0b94e
            self.timeout_message = 'TART'
Packit f0b94e
            self.event.set()
Packit f0b94e
Packit f0b94e
        if not (line.startswith('JavaScript error:') or
Packit f0b94e
                line.startswith('JavaScript warning:')):
Packit f0b94e
            LOG.process_output(self.proc.pid, line)
Packit f0b94e
            self.output.append(line)
Packit f0b94e
Packit f0b94e
Packit f0b94e
def run_browser(command, minidump_dir, timeout=None, on_started=None,
Packit f0b94e
                debug=None, debugger=None, debugger_args=None, **kwargs):
Packit f0b94e
    """
Packit f0b94e
    Run the browser using the given `command`.
Packit f0b94e
Packit f0b94e
    After the browser prints __endTimestamp, we give it 5
Packit f0b94e
    seconds to quit and kill it if it's still alive at that point.
Packit f0b94e
Packit f0b94e
    Note that this method ensure that the process is killed at
Packit f0b94e
    the end. If this is not possible, an exception will be raised.
Packit f0b94e
Packit f0b94e
    :param command: the commad (as a string list) to run the browser
Packit f0b94e
    :param minidump_dir: a path where to extract minidumps in case the
Packit f0b94e
                         browser hang. This have to be the same value
Packit f0b94e
                         used in `mozcrash.check_for_crashes`.
Packit f0b94e
    :param timeout: if specified, timeout to wait for the browser before
Packit f0b94e
                    we raise a :class:`TalosError`
Packit f0b94e
    :param on_started: a callback that can be used to do things just after
Packit f0b94e
                       the browser has been started. The callback must takes
Packit f0b94e
                       an argument, which is the psutil.Process instance
Packit f0b94e
    :param kwargs: additional keyword arguments for the :class:`ProcessHandler`
Packit f0b94e
                   instance
Packit f0b94e
Packit f0b94e
    Returns a ProcessContext instance, with available output and pid used.
Packit f0b94e
    """
Packit f0b94e
Packit f0b94e
    debugger_info = find_debugger_info(debug, debugger, debugger_args)
Packit f0b94e
    if debugger_info is not None:
Packit f0b94e
        return run_in_debug_mode(command, debugger_info,
Packit f0b94e
                                 on_started=on_started, env=kwargs.get('env'))
Packit f0b94e
Packit f0b94e
    context = ProcessContext()
Packit f0b94e
    first_time = int(time.time()) * 1000
Packit f0b94e
    wait_for_quit_timeout = 5
Packit f0b94e
    event = Event()
Packit f0b94e
    reader = Reader(event)
Packit f0b94e
Packit f0b94e
    LOG.info("Using env: %s" % pprint.pformat(kwargs['env']))
Packit f0b94e
Packit f0b94e
    kwargs['storeOutput'] = False
Packit f0b94e
    kwargs['processOutputLine'] = reader
Packit f0b94e
    kwargs['onFinish'] = event.set
Packit f0b94e
    proc = ProcessHandler(command, **kwargs)
Packit f0b94e
    reader.proc = proc
Packit f0b94e
    proc.run()
Packit f0b94e
Packit f0b94e
    LOG.process_start(proc.pid, ' '.join(command))
Packit f0b94e
    try:
Packit f0b94e
        context.process = psutil.Process(proc.pid)
Packit f0b94e
        if on_started:
Packit f0b94e
            on_started(context.process)
Packit f0b94e
        # wait until we saw __endTimestamp in the proc output,
Packit f0b94e
        # or the browser just terminated - or we have a timeout
Packit f0b94e
        if not event.wait(timeout):
Packit f0b94e
            LOG.info("Timeout waiting for test completion; killing browser...")
Packit f0b94e
            # try to extract the minidump stack if the browser hangs
Packit f0b94e
            mozcrash.kill_and_get_minidump(proc.pid, minidump_dir)
Packit f0b94e
            raise TalosError("timeout")
Packit f0b94e
        if reader.got_end_timestamp:
Packit f0b94e
            for i in range(1, wait_for_quit_timeout):
Packit f0b94e
                if proc.wait(1) is not None:
Packit f0b94e
                    break
Packit f0b94e
            if proc.poll() is None:
Packit f0b94e
                LOG.info(
Packit f0b94e
                    "Browser shutdown timed out after {0} seconds, terminating"
Packit f0b94e
                    " process.".format(wait_for_quit_timeout)
Packit f0b94e
                )
Packit f0b94e
        elif reader.got_timeout:
Packit f0b94e
            raise TalosError('TIMEOUT: %s' % reader.timeout_message)
Packit f0b94e
    finally:
Packit f0b94e
        # this also handle KeyboardInterrupt
Packit f0b94e
        # ensure early the process is really terminated
Packit f0b94e
        return_code = None
Packit f0b94e
        try:
Packit f0b94e
            return_code = context.kill_process()
Packit f0b94e
            if return_code is None:
Packit f0b94e
                return_code = proc.wait(1)
Packit f0b94e
        except Exception:
Packit f0b94e
            # Maybe killed by kill_and_get_minidump(), maybe ended?
Packit f0b94e
            LOG.info("Unable to kill process")
Packit f0b94e
            LOG.info(traceback.format_exc())
Packit f0b94e
Packit f0b94e
    reader.output.append(
Packit f0b94e
        "__startBeforeLaunchTimestamp%d__endBeforeLaunchTimestamp"
Packit f0b94e
        % first_time)
Packit f0b94e
    reader.output.append(
Packit f0b94e
        "__startAfterTerminationTimestamp%d__endAfterTerminationTimestamp"
Packit f0b94e
        % (int(time.time()) * 1000))
Packit f0b94e
Packit f0b94e
    if return_code is not None:
Packit f0b94e
        LOG.process_exit(proc.pid, return_code)
Packit f0b94e
    else:
Packit f0b94e
        LOG.debug("Unable to detect exit code of the process %s." % proc.pid)
Packit f0b94e
    context.output = reader.output
Packit f0b94e
    return context
Packit f0b94e
Packit f0b94e
Packit f0b94e
def find_debugger_info(debug, debugger, debugger_args):
Packit f0b94e
    debuggerInfo = None
Packit f0b94e
    if debug or debugger or debugger_args:
Packit f0b94e
        import mozdebug
Packit f0b94e
Packit f0b94e
        if not debugger:
Packit f0b94e
            # No debugger name was provided. Look for the default ones on
Packit f0b94e
            # current OS.
Packit f0b94e
            debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking)
Packit f0b94e
Packit f0b94e
        debuggerInfo = None
Packit f0b94e
        if debugger:
Packit f0b94e
            debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
Packit f0b94e
Packit f0b94e
        if debuggerInfo is None:
Packit f0b94e
            raise TalosError('Could not find a suitable debugger in your PATH.')
Packit f0b94e
Packit f0b94e
    return debuggerInfo
Packit f0b94e
Packit f0b94e
Packit f0b94e
def run_in_debug_mode(command, debugger_info, on_started=None, env=None):
Packit f0b94e
    signal.signal(signal.SIGINT, lambda sigid, frame: None)
Packit f0b94e
    context = ProcessContext()
Packit f0b94e
    command_under_dbg = [debugger_info.path] + debugger_info.args + command
Packit f0b94e
Packit f0b94e
    ttest_process = subprocess.Popen(command_under_dbg, env=env)
Packit f0b94e
Packit f0b94e
    context.process = psutil.Process(ttest_process.pid)
Packit f0b94e
    if on_started:
Packit f0b94e
        on_started(context.process)
Packit f0b94e
Packit f0b94e
    return_code = ttest_process.wait()
Packit f0b94e
Packit f0b94e
    if return_code is not None:
Packit f0b94e
        LOG.process_exit(ttest_process.pid, return_code)
Packit f0b94e
    else:
Packit f0b94e
        LOG.debug("Unable to detect exit code of the process %s." % ttest_process.pid)
Packit f0b94e
Packit f0b94e
    return context