DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (05ff1b051ae2)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.

from __future__ import absolute_import

import os
import subprocess
import sys
import traceback
from abc import ABCMeta, abstractproperty

from mozlog import get_default_logger
from mozprocess import ProcessHandler
from six import string_types

try:
    import mozcrash
except ImportError:
    mozcrash = None
from six import reraise

from ..application import DefaultContext
from ..errors import RunnerNotStartedError

if sys.version_info[0] < 3:
    unicode_type = unicode
else:
    unicode_type = str


class BaseRunner(object):
    """
    The base runner class for all mozrunner objects, both local and remote.
    """
    __metaclass__ = ABCMeta
    last_test = 'mozrunner-startup'
    process_handler = None
    timeout = None
    output_timeout = None

    def __init__(self, app_ctx=None, profile=None, clean_profile=True, env=None,
                 process_class=None, process_args=None, symbols_path=None,
                 dump_save_path=None, addons=None):
        self.app_ctx = app_ctx or DefaultContext()

        if isinstance(profile, string_types):
            self.profile = self.app_ctx.profile_class(profile=profile,
                                                      addons=addons)
        else:
            self.profile = profile or self.app_ctx.profile_class(**getattr(self.app_ctx,
                                                                           'profile_args', {}))

        self.logger = get_default_logger()

        # process environment
        if env is None:
            self.env = os.environ.copy()
        else:
            self.env = env.copy()

        self.clean_profile = clean_profile
        self.process_class = process_class or ProcessHandler
        self.process_args = process_args or {}
        self.symbols_path = symbols_path
        self.dump_save_path = dump_save_path

        self.crashed = 0

    def __del__(self):
        self.cleanup()

    @abstractproperty
    def command(self):
        """Returns the command list to run."""
        pass

    @property
    def returncode(self):
        """
        The returncode of the process_handler. A value of None
        indicates the process is still running. A negative
        value indicates the process was killed with the
        specified signal.

        :raises: RunnerNotStartedError
        """
        if self.process_handler:
            return self.process_handler.poll()
        else:
            raise RunnerNotStartedError("returncode accessed before runner started")

    def start(self, debug_args=None, interactive=False, timeout=None, outputTimeout=None):
        """
        Run self.command in the proper environment.

        :param debug_args: arguments for a debugger
        :param interactive: uses subprocess.Popen directly
        :param timeout: see process_handler.run()
        :param outputTimeout: see process_handler.run()
        :returns: the process id

        :raises: RunnerNotStartedError
        """
        self.timeout = timeout
        self.output_timeout = outputTimeout
        cmd = self.command

        # ensure the runner is stopped
        self.stop()

        # attach a debugger, if specified
        if debug_args:
            cmd = list(debug_args) + cmd

        if self.logger:
            self.logger.info('Application command: %s' % ' '.join(cmd))

        encoded_env = {}
        for k in self.env:
            v = self.env[k]
            if isinstance(v, unicode_type):
                v = v.encode('utf-8')
            if isinstance(k, unicode_type):
                k = k.encode('utf-8')
            encoded_env[k] = v

        if interactive:
            self.process_handler = subprocess.Popen(cmd, env=encoded_env)
            # TODO: other arguments
        else:
            # this run uses the managed processhandler
            try:
                process = self.process_class(cmd, env=encoded_env, **self.process_args)
                process.run(self.timeout, self.output_timeout)

                self.process_handler = process
            except Exception:
                _, value, tb = sys.exc_info()
                reraise(RunnerNotStartedError, "Failed to start the process: %s" % value, tb)

        self.crashed = 0
        return self.process_handler.pid

    def wait(self, timeout=None):
        """
        Wait for the process to exit.

        :param timeout: if not None, will return after timeout seconds.
                        Timeout is ignored if interactive was set to True.
        :returns: the process return code if process exited normally,
                  -<signal> if process was killed (Unix only),
                  None if timeout was reached and the process is still running.
        :raises: RunnerNotStartedError
        """
        if self.is_running():
            # The interactive mode uses directly a Popen process instance. It's
            # wait() method doesn't have any parameters. So handle it separately.
            if isinstance(self.process_handler, subprocess.Popen):
                self.process_handler.wait()
            else:
                self.process_handler.wait(timeout)

        elif not self.process_handler:
            raise RunnerNotStartedError("Wait() called before process started")

        return self.returncode

    def is_running(self):
        """
        Checks if the process is running.

        :returns: True if the process is active
        """
        return self.returncode is None

    def stop(self, sig=None):
        """
        Kill the process.

        :param sig: Signal used to kill the process, defaults to SIGKILL
                    (has no effect on Windows).
        :returns: the process return code if process was already stopped,
                  -<signal> if process was killed (Unix only)
        :raises: RunnerNotStartedError
        """
        try:
            if not self.is_running():
                return self.returncode
        except RunnerNotStartedError:
            return

        # The interactive mode uses directly a Popen process instance. It's
        # kill() method doesn't have any parameters. So handle it separately.
        if isinstance(self.process_handler, subprocess.Popen):
            self.process_handler.kill()
        else:
            self.process_handler.kill(sig=sig)

        return self.returncode

    def reset(self):
        """
        Reset the runner to its default state.
        """
        self.stop()
        self.process_handler = None

    def check_for_crashes(self, dump_directory=None, dump_save_path=None,
                          test_name=None, quiet=False):
        """Check for possible crashes and output the stack traces.

        :param dump_directory: Directory to search for minidump files
        :param dump_save_path: Directory to save the minidump files to
        :param test_name: Name to use in the crash output
        :param quiet: If `True` don't print the PROCESS-CRASH message to stdout

        :returns: Number of crashes which have been detected since the last invocation
        """
        crash_count = 0

        if not dump_directory:
            dump_directory = os.path.join(self.profile.profile, 'minidumps')

        if not dump_save_path:
            dump_save_path = self.dump_save_path

        if not test_name:
            test_name = "runner.py"

        try:
            if self.logger:
                if mozcrash:
                    crash_count = mozcrash.log_crashes(
                        self.logger,
                        dump_directory,
                        self.symbols_path,
                        dump_save_path=dump_save_path,
                        test=test_name)
                else:
                    self.logger.warning("Can not log crashes without mozcrash")
            else:
                if mozcrash:
                    crash_count = mozcrash.check_for_crashes(
                        dump_directory,
                        self.symbols_path,
                        dump_save_path=dump_save_path,
                        test_name=test_name,
                        quiet=quiet)

            self.crashed += crash_count
        except Exception:
            traceback.print_exc()

        return crash_count

    def cleanup(self):
        """
        Cleanup all runner state
        """
        self.stop()