diff options
author | Tor Arne Vestbø <torarnv@gmail.com> | 2017-05-13 15:19:56 +0200 |
---|---|---|
committer | Tor Arne Vestbø <tor.arne.vestbo@qt.io> | 2017-05-19 14:03:46 +0000 |
commit | 9b3ad89dcd7399c59a178f2d66b924432c999985 (patch) | |
tree | 04d77d60dfbb1e3c83046738c65ea5cbac6224c9 /share | |
parent | 4e35cc2ea80effe757ff0c81a739aa1eb761b7c4 (diff) | |
download | qt-creator-9b3ad89dcd7399c59a178f2d66b924432c999985.tar.gz |
Make Qt python pretty printers available inside LLDB/Xcode
The LLBD bridge can be imported from the users's ~/.lldbinit:
command script import "<path to lldbbridge.py>"
Or by relying on a (future) debug script in the QtCore dSYM bundle.
Change-Id: Ia099dcebc6375d38ae2d75c939bb5669e30e4b2c
Reviewed-by: hjk <hjk@qt.io>
Diffstat (limited to 'share')
-rw-r--r-- | share/qtcreator/debugger/README.txt | 9 | ||||
-rw-r--r-- | share/qtcreator/debugger/lldbbridge.py | 453 |
2 files changed, 432 insertions, 30 deletions
diff --git a/share/qtcreator/debugger/README.txt b/share/qtcreator/debugger/README.txt index 8307da3c8d..0913f39f50 100644 --- a/share/qtcreator/debugger/README.txt +++ b/share/qtcreator/debugger/README.txt @@ -1,7 +1,7 @@ While the primary intention of this pretty printing implementation is -to provide what Qt Creator needs, it can be used in a plain commandline -GDB session, too. +to provide what Qt Creator needs, it can be used in a plain GDB and LLDB +session, too. With @@ -65,6 +65,11 @@ With code like ss = <QString> = {"Hello"} +Or for LLDB (.lldbinit or directly in the LLDB interpreter): + + command script import <path/to/qtcreator>/share/qtcreator/debugger/lldbbridge.py + +This will add LLDB summary providers for all the Qt types in a new type category named 'Qt'. diff --git a/share/qtcreator/debugger/lldbbridge.py b/share/qtcreator/debugger/lldbbridge.py index 8287d5e8f9..86a0d3e9bc 100644 --- a/share/qtcreator/debugger/lldbbridge.py +++ b/share/qtcreator/debugger/lldbbridge.py @@ -31,8 +31,14 @@ import sys import threading import lldb +from contextlib import contextmanager + sys.path.insert(1, os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) +# Simplify development of this module by reloading deps +if 'dumper' in sys.modules: + reload(sys.modules['dumper']) + from dumper import * ####################################################################### @@ -52,7 +58,7 @@ def check(exp): raise RuntimeError('Check failed') class Dumper(DumperBase): - def __init__(self): + def __init__(self, debugger = None): DumperBase.__init__(self) lldb.theDumper = self @@ -60,33 +66,38 @@ class Dumper(DumperBase): self.typeCache = {} self.outputLock = threading.Lock() - self.debugger = lldb.SBDebugger.Create() - #self.debugger.SetLoggingCallback(loggingCallback) - #def loggingCallback(args): - # s = args.strip() - # s = s.replace('"', "'") - # sys.stdout.write('log="%s"@\n' % s) - #Same as: self.debugger.HandleCommand('log enable lldb dyld step') - #self.debugger.EnableLog('lldb', ['dyld', 'step', 'process', 'state', - # 'thread', 'events', - # 'communication', 'unwind', 'commands']) - #self.debugger.EnableLog('lldb', ['all']) - self.debugger.Initialize() - self.debugger.HandleCommand('settings set auto-confirm on') - - # FIXME: warn('DISABLING DEFAULT FORMATTERS') - # It doesn't work at all with 179.5 and we have some bad - # interaction in 300 - # if not hasattr(lldb.SBType, 'GetCanonicalType'): # 'Test' for 179.5 - #self.debugger.HandleCommand('type category delete gnu-libstdc++') - #self.debugger.HandleCommand('type category delete libcxx') - #self.debugger.HandleCommand('type category delete default') - self.debugger.DeleteCategory('gnu-libstdc++') - self.debugger.DeleteCategory('libcxx') - self.debugger.DeleteCategory('default') - self.debugger.DeleteCategory('cplusplus') - #for i in range(self.debugger.GetNumCategories()): - # self.debugger.GetCategoryAtIndex(i).SetEnabled(False) + + if debugger: + # Re-use existing debugger + self.debugger = debugger + else: + self.debugger = lldb.SBDebugger.Create() + #self.debugger.SetLoggingCallback(loggingCallback) + #def loggingCallback(args): + # s = args.strip() + # s = s.replace('"', "'") + # sys.stdout.write('log="%s"@\n' % s) + #Same as: self.debugger.HandleCommand('log enable lldb dyld step') + #self.debugger.EnableLog('lldb', ['dyld', 'step', 'process', 'state', + # 'thread', 'events', + # 'communication', 'unwind', 'commands']) + #self.debugger.EnableLog('lldb', ['all']) + self.debugger.Initialize() + self.debugger.HandleCommand('settings set auto-confirm on') + + # FIXME: warn('DISABLING DEFAULT FORMATTERS') + # It doesn't work at all with 179.5 and we have some bad + # interaction in 300 + # if not hasattr(lldb.SBType, 'GetCanonicalType'): # 'Test' for 179.5 + #self.debugger.HandleCommand('type category delete gnu-libstdc++') + #self.debugger.HandleCommand('type category delete libcxx') + #self.debugger.HandleCommand('type category delete default') + self.debugger.DeleteCategory('gnu-libstdc++') + self.debugger.DeleteCategory('libcxx') + self.debugger.DeleteCategory('default') + self.debugger.DeleteCategory('cplusplus') + #for i in range(self.debugger.GetNumCategories()): + # self.debugger.GetCategoryAtIndex(i).SetEnabled(False) self.process = None self.target = None @@ -1770,3 +1781,389 @@ class Tester(Dumper): warn('Cannot determined stopped thread') lldb.SBDebugger.Destroy(self.debugger) + +# ------------------------------ For use in LLDB ------------------------------ + +from pprint import pprint + +__module__ = sys.modules[__name__] +DEBUG = False if not hasattr(__module__, 'DEBUG') else DEBUG + +class LogMixin: + @staticmethod + def log(message = '', log_caller = False, frame = 1, args = ''): + if not DEBUG: + return + if log_caller: + message = ": " + message if len(message) else '' + # FIXME: Compute based on first frame not in this class? + frame = sys._getframe(frame) + fn = frame.f_code.co_name + localz = frame.f_locals + instance = str(localz["self"]) + "." if 'self' in localz else '' + message = "%s%s(%s)%s" % (instance, fn, args, message) + print message + + @staticmethod + def log_fn(arg_str = ''): + LogMixin.log(log_caller = True, frame = 2, args = arg_str) + +class DummyDebugger(object): + def __getattr__(self, attr): + raise AttributeError("Debugger should not be needed to create summaries") + +class SummaryDumper(Dumper, LogMixin): + _instance = None + _lock = threading.RLock() + _type_caches = {} + + @staticmethod + def initialize(): + SummaryDumper._instance = SummaryDumper() + return SummaryDumper._instance + + @staticmethod + @contextmanager + def shared(valobj): + SummaryDumper._lock.acquire() + dumper = SummaryDumper._instance + dumper.target = valobj.target + dumper.process = valobj.process + debugger_id = dumper.target.debugger.GetID() + dumper.typeCache = SummaryDumper._type_caches.get(debugger_id, {}) + yield dumper + SummaryDumper._type_caches[debugger_id] = dumper.typeCache + SummaryDumper._lock.release() + + @staticmethod + def warn(message): + print "Qt summary warning: %s" % message + + @staticmethod + def showException(message, exType, exValue, exTraceback): + # FIXME: Store for later and report back in summary + SummaryDumper.log("Exception during dumping: %s" % exValue) + + def __init__(self): + DumperBase.warn = staticmethod(SummaryDumper.warn) + DumperBase.showException = staticmethod(SummaryDumper.showException) + + Dumper.__init__(self, DummyDebugger()) + + self.setVariableFetchingOptions({ + 'fancy' : True, + 'qobjectnames' : True, + 'passexceptions' : True + }) + + self.dumpermodules = ['qttypes'] + self.loadDumpers({}) + self.output = '' + + def report(self, stuff): + return # Don't mess up lldb output + + def dump_summary(self, valobj, expanded = False): + try: + from pygdbmi import gdbmiparser + except ImportError: + print "Qt summary provider requires the pygdbmi module, " \ + "please install using 'sudo /usr/bin/easy_install pygdbmi', " \ + "and then restart Xcode." + lldb.debugger.HandleCommand('type category delete Qt') + return None + + value = self.fromNativeValue(valobj) + + # Expand variable if we need synthetic children + oldExpanded = self.expandedINames + self.expandedINames = [value.name] if expanded else [] + + savedOutput = self.output + self.output = '' + with TopLevelItem(self, value.name): + self.putItem(value) + + # FIXME: Hook into putField, etc to build up object instead of parsing MI + response = gdbmiparser.parse_response("^ok,summary=%s" % self.output) + + self.output = savedOutput + self.expandedINames = oldExpanded + + summary = response["payload"]["summary"] + + #print value, " --> ", + #pprint(summary) + + return summary + +class SummaryProvider(LogMixin): + + DEFAULT_SUMMARY = '' + VOID_PTR_TYPE = None + + @staticmethod + def provide_summary(valobj, internal_dict, options = None): + if not __name__ in internal_dict: + # When disabling the import of the Qt summary providers, the + # summary functions are still registered with LLDB, and we will + # get callbacks to provide summaries, but at this point the child + # providers are not active, so instead of providing half-baked and + # confusing summaries we opt to unload all the summaries. + SummaryDumper.log("Module '%s' was unloaded, removing Qt category" % __name__) + lldb.debugger.HandleCommand('type category delete Qt') + return SummaryProvider.DEFAULT_SUMMARY + + # FIXME: It would be nice to cache the providers, so that if a + # synthetic child provider has already been created for a valobj, + # we can re-use that when providing summary for the synthetic child + # parent, but we lack a consistent cache key for that to work. + + provider = SummaryProvider(valobj) + provider.update() + + return provider.get_summary(options) + + def __init__(self, valobj, expand = False): + # Prevent recursive evaluation during logging, etc + valobj.SetPreferSyntheticValue(False) + + self.log_fn(valobj.path) + + self.valobj = valobj + self.expand = expand + + if not SummaryProvider.VOID_PTR_TYPE: + with SummaryDumper.shared(self.valobj) as dumper: + SummaryProvider.VOID_PTR_TYPE = dumper.lookupNativeType('void*') + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self.key())) + + def key(self): + if not hasattr(self, 'valobj'): + return None + return self.valobj.path + + def update(self): + self.log_fn() + with SummaryDumper.shared(self.valobj) as dumper: + self.summary = dumper.dump_summary(self.valobj, self.expand) + + def get_summary(self, options): + self.log_fn() + + summary = self.summary + if not summary: + return '<error: could not get summary from Qt provider>' + + encoding = summary.get("valueencoded") + summaryValue = summary["value"] + + # FIXME: Share between Creator and LLDB in the python code + + if encoding: + special_encodings = { + "empty" : "<empty>", + "minimumitemcount" : "<at least %s items>" % summaryValue, + "undefined" : "<undefined>", + "null" : "<null>", + "itemcount" : "<%s items>" % summaryValue, + "notaccessible" : "<not accessible>", + "optimizedout" : "<optimized out>", + "nullreference" : "<null reference>", + "emptystructure" : "<empty structure>", + "uninitialized" : "<uninitialized>", + "invalid" : "<invalid>", + "notcallable" : "<not callable>", + "outofscope" : "<out of scope>" + } + if encoding in special_encodings: + return special_encodings[encoding] + + text_encodings = [ + 'latin1', + # 'local8bit', + 'utf8', + 'utf16', + # 'ucs4' + ] + + if encoding in text_encodings: + try: + binaryvalue = summaryValue.decode('hex') + # LLDB expects UTF-8 + return "\"%s\"" % (binaryvalue.decode(encoding).encode('utf8')) + except: + return "<failed to decode '%s' as '%s'>" % (summaryValue, encoding) + + # FIXME: Support these + other_encodings = ['int', 'uint', 'float', 'juliandate', 'juliandateandmillisecondssincemidnight', + 'millisecondssincemidnight', 'ipv6addressandhexscopeid', 'datetimeinternal'] + + summaryValue += " <encoding='%s'>" % encoding + + if self.valobj.value: + # If we've resolved a pointer that matches what LLDB natively chose, + # then use that instead of printing the values twice. FIXME, ideally + # we'd have the same pointer format as LLDB uses natively. + if re.sub('^0x0*', '', self.valobj.value) == re.sub('^0x0*', '', summaryValue): + return SummaryProvider.DEFAULT_SUMMARY + + return summaryValue.strip() + +class SyntheticChildrenProvider(SummaryProvider): + def __init__(self, valobj, dict): + SummaryProvider.__init__(self, valobj, expand = True) + self.summary = None + self.synthetic_children = [] + + def num_native_children(self): + return self.valobj.GetNumChildren() + + def num_children(self): + self.log("native: %d synthetic: %d" % + (self.num_native_children(), len(self.synthetic_children)), True) + return self._num_children() + + def _num_children(self): + return self.num_native_children() + len(self.synthetic_children) + + def has_children(self): + return self._num_children() > 0 + + def get_child_index(self, name): + # FIXME: Do we ever need this to return something? + self.log_fn(name) + return None + + def get_child_at_index(self, index): + self.log_fn(index) + + if index < 0 or index > self._num_children(): + return None + if not self.valobj.IsValid() or not self.summary: + return None + + if index < self.num_native_children(): + # Built-in children + value = self.valobj.GetChildAtIndex(index) + else: + # Synthetic children + index -= self.num_native_children() + child = self.synthetic_children[index] + name = child.get('name', "[%s]" % index) + value = self.create_value(child, name) + + return value + + def create_value(self, child, name = ''): + child_type = child.get('type', self.summary.get('childtype')) + + value = None + if child_type: + if 'address' in child: + value_type = None + with SummaryDumper.shared(self.valobj) as dumper: + value_type = dumper.lookupNativeType(child_type) + if not value_type or not value_type.IsValid(): + return None + address = int(child['address'], 16) + + # Create as void* so that the value is fully initialized before + # we trigger our own summary/child providers recursively. + value = self.valobj.synthetic_child_from_address(name, address, + SummaryProvider.VOID_PTR_TYPE).Cast(value_type) + else: + self.log("Don't know how to create value for child %s" % child) + # FIXME: Figure out if we ever will hit this case, and deal with it + #value = self.valobj.synthetic_child_from_expression(name, + # "(%s)(%s)" % (child_type, child['value'])) + else: + # FIXME: Find a good way to deal with synthetic values + self.log("Don't know how to create value for child %s" % child) + + # FIXME: Handle value type or value errors consistently + return value + + def update(self): + SummaryProvider.update(self) + + self.synthetic_children = [] + if not 'children' in self.summary: + return + + dereference_child = None + for child in self.summary['children']: + if not child or not type(child) is dict: + continue + + # Skip base classes, they are built-in children + # FIXME: Is there a better check than 'iname'? + if 'iname' in child: + continue + + name = child.get('name') + if name: + if name == '*': + # No use for this unless the built in children failed to resolve + dereference_child = child + continue + + if name.startswith('['): + # Skip pure synthetic children until we can deal with them + continue + + if name.startswith('#'): + # Skip anonymous unions, lookupNativeType doesn't handle them (yet), + # so it triggers the slow lookup path, and the union is provided as + # a native child anyways. + continue + + # Skip native children + if self.valobj.GetChildMemberWithName(name): + continue + + self.synthetic_children.append(child) + + # Xcode will sometimes fail to show the children of pointers in + # the debugger UI, even if dereferencing the pointer works fine. + # We fall back to the special child reported by the Qt dumper. + if not self.valobj.GetNumChildren() and dereference_child: + self.valobj = self.create_value(dereference_child) + self.update() + +def __lldb_init_module(debugger, internal_dict): + # Module is being imported in an LLDB session + dumper = SummaryDumper.initialize() + + type_category = 'Qt' + + # Concrete types + summary_function = "%s.%s.%s" % (__name__, 'SummaryProvider', 'provide_summary') + types = map(lambda x: x.replace('__', '::'), dumper.qqDumpers) + debugger.HandleCommand("type summary add -F %s -w %s %s" \ + % (summary_function, type_category, ' '.join(types))) + + regex_types = map(lambda x: "'" + x + "'", dumper.qqDumpersEx.keys()) + if regex_types: + debugger.HandleCommand("type summary add -x -F %s -w %s %s" \ + % (summary_function, type_category, ' '.join(regex_types))) + + # Global catch-all for Qt classes + debugger.HandleCommand("type summary add -x '^Q.*$' -F %s -w %s" \ + % (summary_function, type_category)) + + # Named summary ('frame variable foo --summary Qt') + debugger.HandleCommand("type summary add --name Qt -F %s -w %s" \ + % (summary_function, type_category)) + + # Synthetic children + debugger.HandleCommand("type synthetic add -x '^Q.*$' -l %s -w %s" \ + % (SyntheticChildrenProvider, type_category)) + + debugger.HandleCommand('type category enable %s' % type_category) + + if not __name__ == 'qt': + # Make available under global 'qt' name for consistency + internal_dict['qt'] = internal_dict[__name__] |