diff options
Diffstat (limited to 'testing/mozbase/mozdebug')
-rw-r--r-- | testing/mozbase/mozdebug/mozdebug/__init__.py | 30 | ||||
-rwxr-xr-x | testing/mozbase/mozdebug/mozdebug/mozdebug.py | 315 | ||||
-rw-r--r-- | testing/mozbase/mozdebug/setup.cfg | 2 | ||||
-rw-r--r-- | testing/mozbase/mozdebug/setup.py | 31 | ||||
-rwxr-xr-x | testing/mozbase/mozdebug/tests/fake_debuggers/cgdb/cgdb | 0 | ||||
-rwxr-xr-x | testing/mozbase/mozdebug/tests/fake_debuggers/devenv/devenv.exe | 0 | ||||
-rwxr-xr-x | testing/mozbase/mozdebug/tests/fake_debuggers/gdb/gdb | 0 | ||||
-rwxr-xr-x | testing/mozbase/mozdebug/tests/fake_debuggers/lldb/lldb | 0 | ||||
-rwxr-xr-x | testing/mozbase/mozdebug/tests/fake_debuggers/wdexpress/wdexpress.exe | 0 | ||||
-rw-r--r-- | testing/mozbase/mozdebug/tests/manifest.ini | 3 | ||||
-rw-r--r-- | testing/mozbase/mozdebug/tests/test.py | 65 |
11 files changed, 446 insertions, 0 deletions
diff --git a/testing/mozbase/mozdebug/mozdebug/__init__.py b/testing/mozbase/mozdebug/mozdebug/__init__.py new file mode 100644 index 0000000000..bb8711e2c4 --- /dev/null +++ b/testing/mozbase/mozdebug/mozdebug/__init__.py @@ -0,0 +1,30 @@ +# flake8: noqa +# 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/. + +""" +This module contains a set of function to gather information about the +debugging capabilities of the platform. It allows to look for a specific +debugger or to query the system for a compatible/default debugger. + +The following simple example looks for the default debugger on the +current platform and launches a debugger process with the correct +debugger-specific arguments: + +:: + + import mozdebug + + debugger = mozdebug.get_default_debugger_name() + debuggerInfo = mozdebug.get_debugger_info(debugger) + + debuggeePath = "toDebug" + + processArgs = [self.debuggerInfo.path] + self.debuggerInfo.args + processArgs.append(debuggeePath) + + run_process(args, ...) + +""" +from .mozdebug import * diff --git a/testing/mozbase/mozdebug/mozdebug/mozdebug.py b/testing/mozbase/mozdebug/mozdebug/mozdebug.py new file mode 100755 index 0000000000..beecc2cd9d --- /dev/null +++ b/testing/mozbase/mozdebug/mozdebug/mozdebug.py @@ -0,0 +1,315 @@ +#!/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 json +import os +import sys +from collections import namedtuple +from distutils.spawn import find_executable +from subprocess import check_output + +import mozinfo + +__all__ = [ + "get_debugger_info", + "get_default_debugger_name", + "DebuggerSearch", + "get_default_valgrind_args", + "DebuggerInfo", +] + +""" +Map of debugging programs to information about them, like default arguments +and whether or not they are interactive. + +To add support for a new debugger, simply add the relative entry in +_DEBUGGER_INFO and optionally update the _DEBUGGER_PRIORITIES. +""" +_DEBUGGER_INFO = { + # gdb requires that you supply the '--args' flag in order to pass arguments + # after the executable name to the executable. + "gdb": {"interactive": True, "args": ["-q", "--args"]}, + "cgdb": {"interactive": True, "args": ["-q", "--args"]}, + "rust-gdb": {"interactive": True, "args": ["-q", "--args"]}, + "lldb": {"interactive": True, "args": ["--"], "requiresEscapedArgs": True}, + # Visual Studio Debugger Support. + "devenv.exe": {"interactive": True, "args": ["-debugexe"]}, + # Visual C++ Express Debugger Support. + "wdexpress.exe": {"interactive": True, "args": ["-debugexe"]}, + # Windows Development Kit super-debugger. + "windbg.exe": { + "interactive": True, + }, +} + +# Maps each OS platform to the preferred debugger programs found in _DEBUGGER_INFO. +_DEBUGGER_PRIORITIES = { + "win": ["devenv.exe", "wdexpress.exe"], + "linux": ["gdb", "cgdb", "lldb"], + "mac": ["lldb", "gdb"], + "android": ["lldb"], + "unknown": ["gdb"], +} + + +DebuggerInfo = namedtuple( + "DebuggerInfo", ["path", "interactive", "args", "requiresEscapedArgs"] +) + + +def _windbg_installation_paths(): + programFilesSuffixes = ["", " (x86)"] + programFiles = "C:/Program Files" + # Try the most recent versions first. + windowsKitsVersions = ["10", "8.1", "8"] + + for suffix in programFilesSuffixes: + windowsKitsPrefix = os.path.join(programFiles + suffix, "Windows Kits") + for version in windowsKitsVersions: + yield os.path.join( + windowsKitsPrefix, version, "Debuggers", "x64", "windbg.exe" + ) + + +def _vswhere_path(): + try: + import buildconfig + + path = os.path.join(buildconfig.topsrcdir, "build", "win32", "vswhere.exe") + if os.path.isfile(path): + return path + except ImportError: + pass + # Hope it's available on PATH! + return "vswhere.exe" + + +def get_debugger_path(debugger): + """ + Get the full path of the debugger. + + :param debugger: The name of the debugger. + """ + + if mozinfo.os == "mac" and debugger == "lldb": + # On newer OSX versions System Integrity Protections prevents us from + # setting certain env vars for a process such as DYLD_LIBRARY_PATH if + # it's in a protected directory such as /usr/bin. This is the case for + # lldb, so we try to find an instance under the Xcode install instead. + + # Attempt to use the xcrun util to find the path. + try: + path = check_output( + ["xcrun", "--find", "lldb"], universal_newlines=True + ).strip() + if path: + return path + except Exception: + # Just default to find_executable instead. + pass + + if mozinfo.os == "win" and debugger == "devenv.exe": + # Attempt to use vswhere to find the path. + try: + encoding = "mbcs" if sys.platform == "win32" else "utf-8" + vswhere = _vswhere_path() + vsinfo = check_output([vswhere, "-format", "json", "-latest"]) + vsinfo = json.loads(vsinfo.decode(encoding, "replace")) + return os.path.join( + vsinfo[0]["installationPath"], "Common7", "IDE", "devenv.exe" + ) + except Exception: + # Just default to find_executable instead. + pass + + return find_executable(debugger) + + +def get_debugger_info(debugger, debuggerArgs=None, debuggerInteractive=False): + """ + Get the information about the requested debugger. + + Returns a dictionary containing the ``path`` of the debugger executable, + if it will run in ``interactive`` mode, its arguments and whether it needs + to escape arguments it passes to the debugged program (``requiresEscapedArgs``). + If the debugger cannot be found in the system, returns ``None``. + + :param debugger: The name of the debugger. + :param debuggerArgs: If specified, it's the arguments to pass to the debugger, + as a string. Any debugger-specific separator arguments are appended after + these arguments. + :param debuggerInteractive: If specified, forces the debugger to be interactive. + """ + + debuggerPath = None + + if debugger: + # Append '.exe' to the debugger on Windows if it's not present, + # so things like '--debugger=devenv' work. + if os.name == "nt" and not debugger.lower().endswith(".exe"): + debugger += ".exe" + + debuggerPath = get_debugger_path(debugger) + + if not debuggerPath: + # windbg is not installed with the standard set of tools, and it's + # entirely possible that the user hasn't added the install location to + # PATH, so we have to be a little more clever than normal to locate it. + # Just try to look for it in the standard installed location(s). + if debugger == "windbg.exe": + for candidate in _windbg_installation_paths(): + if os.path.exists(candidate): + debuggerPath = candidate + break + else: + if os.path.exists(debugger): + debuggerPath = debugger + + if not debuggerPath: + print("Error: Could not find debugger %s." % debugger) + print("Is it installed? Is it in your PATH?") + return None + + debuggerName = os.path.basename(debuggerPath).lower() + + def get_debugger_info(type, default): + if debuggerName in _DEBUGGER_INFO and type in _DEBUGGER_INFO[debuggerName]: + return _DEBUGGER_INFO[debuggerName][type] + return default + + # Define a namedtuple to access the debugger information from the outside world. + debugger_arguments = [] + + if debuggerArgs: + # Append the provided debugger arguments at the end of the arguments list. + debugger_arguments += debuggerArgs.split() + + debugger_arguments += get_debugger_info("args", []) + + # Override the default debugger interactive mode if needed. + debugger_interactive = get_debugger_info("interactive", False) + if debuggerInteractive: + debugger_interactive = debuggerInteractive + + d = DebuggerInfo( + debuggerPath, + debugger_interactive, + debugger_arguments, + get_debugger_info("requiresEscapedArgs", False), + ) + + return d + + +# Defines the search policies to use in get_default_debugger_name. + + +class DebuggerSearch: + OnlyFirst = 1 + KeepLooking = 2 + + +def get_default_debugger_name(search=DebuggerSearch.OnlyFirst): + """ + Get the debugger name for the default debugger on current platform. + + :param search: If specified, stops looking for the debugger if the + default one is not found (``DebuggerSearch.OnlyFirst``) or keeps + looking for other compatible debuggers (``DebuggerSearch.KeepLooking``). + """ + + mozinfo.find_and_update_from_json() + os = mozinfo.info["os"] + + # Find out which debuggers are preferred for use on this platform. + debuggerPriorities = _DEBUGGER_PRIORITIES[ + os if os in _DEBUGGER_PRIORITIES else "unknown" + ] + + # Finally get the debugger information. + for debuggerName in debuggerPriorities: + debuggerPath = get_debugger_path(debuggerName) + if debuggerPath: + return debuggerName + elif not search == DebuggerSearch.KeepLooking: + return None + + return None + + +# Defines default values for Valgrind flags. +# +# --smc-check=all-non-file is required to deal with code generation and +# patching by the various JITS. Note that this is only necessary on +# x86 and x86_64, but not on ARM. This flag is only necessary for +# Valgrind versions prior to 3.11. +# +# --vex-iropt-register-updates=allregs-at-mem-access is required so that +# Valgrind generates correct register values whenever there is a +# segfault that is caught and handled. In particular OdinMonkey +# requires this. More recent Valgrinds (3.11 and later) provide +# --px-default=allregs-at-mem-access and +# --px-file-backed=unwindregs-at-mem-access +# which provide a significantly cheaper alternative, by restricting the +# precise exception behaviour to JIT generated code only. +# +# --trace-children=yes is required to get Valgrind to follow into +# content and other child processes. The resulting output can be +# difficult to make sense of, and --child-silent-after-fork=yes +# helps by causing Valgrind to be silent for the child in the period +# after fork() but before its subsequent exec(). +# +# --trace-children-skip lists processes that we are not interested +# in tracing into. +# +# --leak-check=full requests full stack traces for all leaked blocks +# detected at process exit. +# +# --show-possibly-lost=no requests blocks for which only an interior +# pointer was found to be considered not leaked. +# +# +# TODO: pass in the user supplied args for V (--valgrind-args=) and +# use this to detect if a different tool has been selected. If so +# adjust tool-specific args appropriately. +# +# TODO: pass in the path to the Valgrind to be used (--valgrind=), and +# check what flags it accepts. Possible args that might be beneficial: +# +# --num-transtab-sectors=24 [reduces re-jitting overheads in long runs] +# --px-default=allregs-at-mem-access +# --px-file-backed=unwindregs-at-mem-access +# [these reduce PX overheads as described above] +# + + +def get_default_valgrind_args(): + return [ + "--fair-sched=yes", + "--smc-check=all-non-file", + "--vex-iropt-register-updates=allregs-at-mem-access", + "--trace-children=yes", + "--child-silent-after-fork=yes", + ( + "--trace-children-skip=" + + "/usr/bin/hg,/bin/rm,*/bin/certutil,*/bin/pk12util," + + "*/bin/ssltunnel,*/bin/uname,*/bin/which,*/bin/ps," + + "*/bin/grep,*/bin/java,*/bin/lsb_release" + ), + ] + get_default_valgrind_tool_specific_args() + + +# The default tool is Memcheck. Feeding these arguments to a different +# Valgrind tool will cause it to fail at startup, so don't do that! + + +def get_default_valgrind_tool_specific_args(): + return [ + "--partial-loads-ok=yes", + "--leak-check=summary", + "--show-possibly-lost=no", + "--show-mismatched-frees=no", + ] diff --git a/testing/mozbase/mozdebug/setup.cfg b/testing/mozbase/mozdebug/setup.cfg new file mode 100644 index 0000000000..3c6e79cf31 --- /dev/null +++ b/testing/mozbase/mozdebug/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/testing/mozbase/mozdebug/setup.py b/testing/mozbase/mozdebug/setup.py new file mode 100644 index 0000000000..2e28924fad --- /dev/null +++ b/testing/mozbase/mozdebug/setup.py @@ -0,0 +1,31 @@ +# 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 setuptools import setup + +PACKAGE_VERSION = "0.3.0" +DEPS = ["mozinfo"] + + +setup( + name="mozdebug", + version=PACKAGE_VERSION, + description="Utilities for running applications under native code debuggers " + "intended for use in Mozilla testing", + long_description="see https://firefox-source-docs.mozilla.org/mozbase/index.html", + classifiers=[ + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.6", + ], + # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers + keywords="mozilla", + author="Mozilla Automation and Testing Team", + author_email="tools@lists.mozilla.org", + url="https://wiki.mozilla.org/Auto-tools/Projects/Mozbase", + license="MPL", + packages=["mozdebug"], + include_package_data=True, + zip_safe=False, + install_requires=DEPS, +) diff --git a/testing/mozbase/mozdebug/tests/fake_debuggers/cgdb/cgdb b/testing/mozbase/mozdebug/tests/fake_debuggers/cgdb/cgdb new file mode 100755 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/mozbase/mozdebug/tests/fake_debuggers/cgdb/cgdb diff --git a/testing/mozbase/mozdebug/tests/fake_debuggers/devenv/devenv.exe b/testing/mozbase/mozdebug/tests/fake_debuggers/devenv/devenv.exe new file mode 100755 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/mozbase/mozdebug/tests/fake_debuggers/devenv/devenv.exe diff --git a/testing/mozbase/mozdebug/tests/fake_debuggers/gdb/gdb b/testing/mozbase/mozdebug/tests/fake_debuggers/gdb/gdb new file mode 100755 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/mozbase/mozdebug/tests/fake_debuggers/gdb/gdb diff --git a/testing/mozbase/mozdebug/tests/fake_debuggers/lldb/lldb b/testing/mozbase/mozdebug/tests/fake_debuggers/lldb/lldb new file mode 100755 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/mozbase/mozdebug/tests/fake_debuggers/lldb/lldb diff --git a/testing/mozbase/mozdebug/tests/fake_debuggers/wdexpress/wdexpress.exe b/testing/mozbase/mozdebug/tests/fake_debuggers/wdexpress/wdexpress.exe new file mode 100755 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/mozbase/mozdebug/tests/fake_debuggers/wdexpress/wdexpress.exe diff --git a/testing/mozbase/mozdebug/tests/manifest.ini b/testing/mozbase/mozdebug/tests/manifest.ini new file mode 100644 index 0000000000..72aff7539a --- /dev/null +++ b/testing/mozbase/mozdebug/tests/manifest.ini @@ -0,0 +1,3 @@ +[DEFAULT] +subsuite = mozbase +[test.py] diff --git a/testing/mozbase/mozdebug/tests/test.py b/testing/mozbase/mozdebug/tests/test.py new file mode 100644 index 0000000000..57bbfec95d --- /dev/null +++ b/testing/mozbase/mozdebug/tests/test.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +import os + +import mozunit +import pytest +from mozdebug.mozdebug import ( + _DEBUGGER_PRIORITIES, + DebuggerSearch, + get_default_debugger_name, +) + +here = os.path.abspath(os.path.dirname(__file__)) + + +@pytest.fixture +def set_debuggers(monkeypatch): + debugger_dir = os.path.join(here, "fake_debuggers") + + def _set_debuggers(*debuggers): + dirs = [] + for d in debuggers: + if d.endswith(".exe"): + d = d[: -len(".exe")] + dirs.append(os.path.join(debugger_dir, d)) + monkeypatch.setenv("PATH", os.pathsep.join(dirs)) + + return _set_debuggers + + +@pytest.mark.parametrize("os_name", ["android", "linux", "mac", "win", "unknown"]) +def test_default_debugger_name(os_name, set_debuggers, monkeypatch): + import sys + + import mozinfo + + def update_os_name(*args, **kwargs): + mozinfo.info["os"] = os_name + + monkeypatch.setattr(mozinfo, "find_and_update_from_json", update_os_name) + + if sys.platform == "win32": + # This is used so distutils.spawn.find_executable doesn't add '.exe' + # suffixes to all our dummy binaries on Windows. + monkeypatch.setattr(sys, "platform", "linux") + + debuggers = _DEBUGGER_PRIORITIES[os_name][:] + debuggers.reverse() + first = True + while len(debuggers) > 0: + set_debuggers(*debuggers) + + if first: + assert get_default_debugger_name() == debuggers[-1] + first = False + else: + assert get_default_debugger_name() is None + assert ( + get_default_debugger_name(DebuggerSearch.KeepLooking) == debuggers[-1] + ) + debuggers = debuggers[:-1] + + +if __name__ == "__main__": + mozunit.main() |