summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozdebug/mozdebug/mozdebug.py
blob: d8d0b8579564f34525d9e9ef285dcc1fb1e4cfcd (plain)
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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
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 shutil
import sys
from collections import namedtuple
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 shutil.which 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 shutil.which instead.
            pass

    return shutil.which(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",
    ]