Source code

Revision control

Copy as Markdown

Other Tools

#!/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/.
import os
import subprocess
import sys
import traceback
from abc import ABCMeta, abstractproperty
import six
from mozlog import get_default_logger
from mozprocess import ProcessHandler
from six import ensure_str, string_types
try:
import mozcrash
except ImportError:
mozcrash = None
from six import reraise
from ..application import DefaultContext
from ..errors import RunnerNotStartedError
@six.add_metaclass(ABCMeta)
class BaseRunner(object):
"""
The base runner class for all mozrunner objects, both local and remote.
"""
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,
explicit_cleanup=False,
):
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
self.explicit_cleanup = explicit_cleanup
def __del__(self):
if not self.explicit_cleanup:
# If we're relying on the gc for cleanup do the same with the profile
self.cleanup(keep_profile=True)
@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))
str_env = {}
for k in self.env:
v = self.env[k]
str_env[ensure_str(k)] = ensure_str(v)
if interactive:
self.process_handler = subprocess.Popen(cmd, env=str_env)
# TODO: other arguments
else:
# this run uses the managed processhandler
try:
process = self.process_class(cmd, env=str_env, **self.process_args)
process.run(self.timeout, self.output_timeout)
self.process_handler = process
except Exception as e:
reraise(
RunnerNotStartedError,
RunnerNotStartedError("Failed to start the process: {}".format(e)),
sys.exc_info()[2],
)
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, timeout=None):
"""
Kill the process.
:param sig: Signal used to kill the process, defaults to SIGKILL
(has no effect on Windows).
:param timeout: Maximum time to wait for the processs to exit
(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, timeout=timeout)
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, keep_profile=False):
"""
Cleanup all runner state
"""
self.stop()
if not keep_profile:
self.profile.cleanup()