auto merge of #4083 : mttr/servo/mach_debugger_flag, r=mbrubeck

Borrows two python modules from mozilla-central to give mach the ability to detect and pass arguments to a system's preferred debugger, and adds `--debug` and `--debugger` flags to `./mach run`. This works almost like the functionality described [here](https://developer.mozilla.org/en-US/docs/Debugging_Mozilla_with_gdb), but at the moment it lacks a `--debugparams`  argument.

Links to borrowed files:
http://hg.mozilla.org/mozilla-central/file/c9cfa9b91dea/testing/mozbase/mozinfo/mozinfo/mozinfo.py
http://hg.mozilla.org/mozilla-central/file/c9cfa9b91dea/testing/mozbase/mozdebug/mozdebug/mozdebug.py
This commit is contained in:
bors-servo 2014-11-24 10:15:46 -07:00
commit 989efd5a28
6 changed files with 524 additions and 3 deletions

View file

@ -11,6 +11,8 @@ import sys
SEARCH_PATHS = [ SEARCH_PATHS = [
"python/mach", "python/mach",
"python/toml", "python/toml",
"python/mozinfo",
"python/mozdebug",
] ]
# Individual files providing mach commands. # Individual files providing mach commands.

View file

@ -0,0 +1,30 @@
# 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 *

168
python/mozdebug/mozdebug.py Normal file
View file

@ -0,0 +1,168 @@
#!/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 mozinfo
from collections import namedtuple
from distutils.spawn import find_executable
__all__ = ['get_debugger_info',
'get_default_debugger_name',
'DebuggerSearch']
'''
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']
},
'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']
},
# valgrind doesn't explain much about leaks unless you set the
# '--leak-check=full' flag. But there are a lot of objects that are
# semi-deliberately leaked, so we set '--show-possibly-lost=no' to avoid
# uninteresting output from those objects. We set '--smc-check==all-non-file'
# and '--vex-iropt-register-updates=allregs-at-mem-access' so that valgrind
# deals properly with JIT'd JavaScript code.
'valgrind': {
'interactive': False,
'args': ['--leak-check=full',
'--show-possibly-lost=no',
'--smc-check=all-non-file',
'--vex-iropt-register-updates=allregs-at-mem-access']
}
}
# 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'],
'unknown': ['gdb']
}
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 = find_executable(debugger)
if not debuggerPath:
print 'Error: Could not find debugger %s.' % debugger
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.
DebuggerInfo = namedtuple(
'DebuggerInfo',
['path', 'interactive', 'args', 'requiresEscapedArgs']
)
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|).
'''
# Find out which debuggers are preferred for use on this platform.
debuggerPriorities = _DEBUGGER_PRIORITIES[mozinfo.os if mozinfo.os in _DEBUGGER_PRIORITIES else 'unknown']
# Finally get the debugger information.
for debuggerName in debuggerPriorities:
debuggerPath = find_executable(debuggerName)
if debuggerPath:
return debuggerName
elif not search == DebuggerSearch.KeepLooking:
return None
return None

View file

@ -0,0 +1,56 @@
# 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/.
"""
interface to transform introspected system information to a format palatable to
Mozilla
Module variables:
.. attribute:: bits
32 or 64
.. attribute:: isBsd
Returns ``True`` if the operating system is BSD
.. attribute:: isLinux
Returns ``True`` if the operating system is Linux
.. attribute:: isMac
Returns ``True`` if the operating system is Mac
.. attribute:: isWin
Returns ``True`` if the operating system is Windows
.. attribute:: os
Operating system [``'win'``, ``'mac'``, ``'linux'``, ...]
.. attribute:: processor
Processor architecture [``'x86'``, ``'x86_64'``, ``'ppc'``, ...]
.. attribute:: version
Operating system version string. For windows, the service pack information is also included
.. attribute:: info
Returns information identifying the current system.
* :attr:`bits`
* :attr:`os`
* :attr:`processor`
* :attr:`version`
"""
import mozinfo
from mozinfo import *
__all__ = mozinfo.__all__

233
python/mozinfo/mozinfo.py Executable file
View file

@ -0,0 +1,233 @@
#!/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/.
# TODO: it might be a good idea of adding a system name (e.g. 'Ubuntu' for
# linux) to the information; I certainly wouldn't want anyone parsing this
# information and having behaviour depend on it
import os
import platform
import re
import sys
# keep a copy of the os module since updating globals overrides this
_os = os
class unknown(object):
"""marker class for unknown information"""
def __nonzero__(self):
return False
def __str__(self):
return 'UNKNOWN'
unknown = unknown() # singleton
# get system information
info = {'os': unknown,
'processor': unknown,
'version': unknown,
'os_version': unknown,
'bits': unknown,
'has_sandbox': unknown }
(system, node, release, version, machine, processor) = platform.uname()
(bits, linkage) = platform.architecture()
# get os information and related data
if system in ["Microsoft", "Windows"]:
info['os'] = 'win'
# There is a Python bug on Windows to determine platform values
# http://bugs.python.org/issue7860
if "PROCESSOR_ARCHITEW6432" in os.environ:
processor = os.environ.get("PROCESSOR_ARCHITEW6432", processor)
else:
processor = os.environ.get('PROCESSOR_ARCHITECTURE', processor)
system = os.environ.get("OS", system).replace('_', ' ')
(major, minor, _, _, service_pack) = os.sys.getwindowsversion()
info['service_pack'] = service_pack
os_version = "%d.%d" % (major, minor)
elif system == "Linux":
if hasattr(platform, "linux_distribution"):
(distro, os_version, codename) = platform.linux_distribution()
else:
(distro, os_version, codename) = platform.dist()
if not processor:
processor = machine
version = "%s %s" % (distro, os_version)
info['os'] = 'linux'
info['linux_distro'] = distro
elif system in ['DragonFly', 'FreeBSD', 'NetBSD', 'OpenBSD']:
info['os'] = 'bsd'
version = os_version = sys.platform
elif system == "Darwin":
(release, versioninfo, machine) = platform.mac_ver()
version = "OS X %s" % release
versionNums = release.split('.')[:2]
os_version = "%s.%s" % (versionNums[0], versionNums[1])
info['os'] = 'mac'
elif sys.platform in ('solaris', 'sunos5'):
info['os'] = 'unix'
os_version = version = sys.platform
else:
os_version = version = unknown
info['version'] = version
info['os_version'] = os_version
# processor type and bits
if processor in ["i386", "i686"]:
if bits == "32bit":
processor = "x86"
elif bits == "64bit":
processor = "x86_64"
elif processor.upper() == "AMD64":
bits = "64bit"
processor = "x86_64"
elif processor == "Power Macintosh":
processor = "ppc"
bits = re.search('(\d+)bit', bits).group(1)
info.update({'processor': processor,
'bits': int(bits),
})
if info['os'] == 'linux':
import ctypes
import errno
PR_SET_SECCOMP = 22
SECCOMP_MODE_FILTER = 2
ctypes.CDLL("libc.so.6", use_errno=True).prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, 0)
info['has_sandbox'] = ctypes.get_errno() == errno.EFAULT
else:
info['has_sandbox'] = True
# standard value of choices, for easy inspection
choices = {'os': ['linux', 'bsd', 'win', 'mac', 'unix'],
'bits': [32, 64],
'processor': ['x86', 'x86_64', 'ppc']}
def sanitize(info):
"""Do some sanitization of input values, primarily
to handle universal Mac builds."""
if "processor" in info and info["processor"] == "universal-x86-x86_64":
# If we're running on OS X 10.6 or newer, assume 64-bit
if release[:4] >= "10.6": # Note this is a string comparison
info["processor"] = "x86_64"
info["bits"] = 64
else:
info["processor"] = "x86"
info["bits"] = 32
# method for updating information
def update(new_info):
"""
Update the info.
:param new_info: Either a dict containing the new info or a path/url
to a json file containing the new info.
"""
if isinstance(new_info, basestring):
# lazy import
import mozfile
import json
f = mozfile.load(new_info)
new_info = json.loads(f.read())
f.close()
info.update(new_info)
sanitize(info)
globals().update(info)
# convenience data for os access
for os_name in choices['os']:
globals()['is' + os_name.title()] = info['os'] == os_name
# unix is special
if isLinux or isBsd:
globals()['isUnix'] = True
def find_and_update_from_json(*dirs):
"""
Find a mozinfo.json file, load it, and update the info with the
contents.
:param dirs: Directories in which to look for the file. They will be
searched after first looking in the root of the objdir
if the current script is being run from a Mozilla objdir.
Returns the full path to mozinfo.json if it was found, or None otherwise.
"""
# First, see if we're in an objdir
try:
from mozbuild.base import MozbuildObject, BuildEnvironmentNotFoundException
build = MozbuildObject.from_environment()
json_path = _os.path.join(build.topobjdir, "mozinfo.json")
if _os.path.isfile(json_path):
update(json_path)
return json_path
except ImportError:
pass
except BuildEnvironmentNotFoundException:
pass
for d in dirs:
d = _os.path.abspath(d)
json_path = _os.path.join(d, "mozinfo.json")
if _os.path.isfile(json_path):
update(json_path)
return json_path
return None
update({})
# exports
__all__ = info.keys()
__all__ += ['is' + os_name.title() for os_name in choices['os']]
__all__ += [
'info',
'unknown',
'main',
'choices',
'update',
'find_and_update_from_json',
]
def main(args=None):
# parse the command line
from optparse import OptionParser
parser = OptionParser(description=__doc__)
for key in choices:
parser.add_option('--%s' % key, dest=key,
action='store_true', default=False,
help="display choices for %s" % key)
options, args = parser.parse_args()
# args are JSON blobs to override info
if args:
# lazy import
import json
for arg in args:
if _os.path.exists(arg):
string = file(arg).read()
else:
string = arg
update(json.loads(string))
# print out choices if requested
flag = False
for key, value in options.__dict__.items():
if value is True:
print '%s choices: %s' % (key, ' '.join([str(choice)
for choice in choices[key]]))
flag = True
if flag: return
# otherwise, print out all info
for key, value in info.items():
print '%s: %s' % (key, value)
if __name__ == '__main__':
main()

View file

@ -1,10 +1,12 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import argparse
import os.path as path import os.path as path
from os import chdir from os import chdir
import subprocess import subprocess
import SimpleHTTPServer import SimpleHTTPServer
import SocketServer import SocketServer
import mozdebug
from shutil import copytree, rmtree, ignore_patterns from shutil import copytree, rmtree, ignore_patterns
from mach.decorators import ( from mach.decorators import (
@ -21,14 +23,44 @@ class MachCommands(CommandBase):
@Command('run', @Command('run',
description='Run Servo', description='Run Servo',
category='post-build') category='post-build')
@CommandArgument('--debug', action='store_true',
help='Enable the debugger. Not specifying a '
'--debugger option will result in the default '
'debugger being used. The following arguments '
'have no effect without this.')
@CommandArgument('--debugger', default=None, type=str,
help='Name of debugger to use.')
@CommandArgument( @CommandArgument(
'params', nargs='...', 'params', nargs='...',
help="Command-line arguments to be passed through to Servo") help="Command-line arguments to be passed through to Servo")
def run(self, params): def run(self, params, debug=False, debugger=None):
env = self.build_env() env = self.build_env()
env["RUST_BACKTRACE"] = "1" env["RUST_BACKTRACE"] = "1"
subprocess.check_call([path.join("target", "servo")] + params,
env=env) args = [path.join("target", "servo")]
# Borrowed and modified from:
# http://hg.mozilla.org/mozilla-central/file/c9cfa9b91dea/python/mozbuild/mozbuild/mach_commands.py#l883
if debug:
import mozdebug
if not debugger:
# No debugger name was provided. Look for the default ones on
# current OS.
debugger = mozdebug.get_default_debugger_name(
mozdebug.DebuggerSearch.KeepLooking)
self.debuggerInfo = mozdebug.get_debugger_info(debugger)
if not self.debuggerInfo:
print("Could not find a suitable debugger in your PATH.")
return 1
# Prepend the debugger args.
args = ([self.debuggerInfo.path] + self.debuggerInfo.args
+ args + params)
else:
args = args + params
subprocess.check_call(args, env=env)
@Command('doc', @Command('doc',
description='Generate documentation', description='Generate documentation',