#!/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", ]