diff --git a/README.md b/README.md index bcbe2fe..08256db 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [[Installation](#installation) • [Commands](#commands) • [Custom Commands](#custom-commands) • [Development Workflow](#development-workflow) [Contributing](#contributing) • [License](#license)] +For a comprehensive overview of LLDB, and how Chisel compliments it, read Ari Grant's [Dancing in the Debugger — A Waltz with LLDB](http://www.objc.io/issue-19/lldb-debugging.html) in issue 19 of [objc.io](http://www.objc.io/). + ## Installation ``` diff --git a/commands/FBAutoLayoutCommands.py b/commands/FBAutoLayoutCommands.py index 2264152..5510d2b 100644 --- a/commands/FBAutoLayoutCommands.py +++ b/commands/FBAutoLayoutCommands.py @@ -36,13 +36,13 @@ def run(self, arguments, options): def setBorderOnAmbiguousViewRecursive(view, width, color): if not fb.evaluateBooleanExpression('[(id)%s isKindOfClass:(Class)[UIView class]]' % view): return - + isAmbiguous = fb.evaluateBooleanExpression('(BOOL)[%s hasAmbiguousLayout]' % view) if isAmbiguous: layer = viewHelpers.convertToLayer(view) lldb.debugger.HandleCommand('expr (void)[%s setBorderWidth:(CGFloat)%s]' % (layer, width)) lldb.debugger.HandleCommand('expr (void)[%s setBorderColor:(CGColorRef)[(id)[UIColor %sColor] CGColor]]' % (layer, color)) - + subviews = fb.evaluateExpression('(id)[%s subviews]' % view) subviewsCount = int(fb.evaluateExpression('(int)[(id)%s count]' % subviews)) if subviewsCount > 0: diff --git a/commands/FBDebugCommands.py b/commands/FBDebugCommands.py index 14b8065..e0ffaa1 100644 --- a/commands/FBDebugCommands.py +++ b/commands/FBDebugCommands.py @@ -31,12 +31,12 @@ def run(self, arguments, options): objectAddress = int(fb.evaluateObjectExpression(commandForObject), 0) - ivarOffsetCommand = '(ptrdiff_t)ivar_getOffset((Ivar)object_getInstanceVariable((id){}, "{}", 0))'.format(objectAddress, ivarName) + ivarOffsetCommand = '(ptrdiff_t)ivar_getOffset((void*)object_getInstanceVariable((id){}, "{}", 0))'.format(objectAddress, ivarName) ivarOffset = fb.evaluateIntegerExpression(ivarOffsetCommand) # A multi-statement command allows for variables scoped to the command, not permanent in the session like $variables. ivarSizeCommand = ('unsigned int size = 0;' - 'char *typeEncoding = (char *)ivar_getTypeEncoding((Ivar)class_getInstanceVariable((Class)object_getClass((id){}), "{}"));' + 'char *typeEncoding = (char *)ivar_getTypeEncoding((void*)class_getInstanceVariable((Class)object_getClass((id){}), "{}"));' '(char *)NSGetSizeAndAlignment(typeEncoding, &size, 0);' 'size').format(objectAddress, ivarName) ivarSize = int(fb.evaluateExpression(ivarSizeCommand), 0) @@ -82,7 +82,17 @@ def args(self): def run(self, arguments, options): expression = arguments[0] - match = re.match(r'([-+])*\[(.*) (.*)\]', expression) + methodPattern = re.compile(r""" + (?P[-+])? + \[ + (?P.*?) + (?P\(.+\))? + \s+ + (?P.*) + \] +""", re.VERBOSE) + + match = methodPattern.match(expression) if not match: print 'Failed to parse expression. Do you even Objective-C?!' @@ -93,9 +103,10 @@ def run(self, arguments, options): print 'Your architecture, {}, is truly fantastic. However, I don\'t currently support it.'.format(arch) return - methodTypeCharacter = match.group(1) - classNameOrExpression = match.group(2) - selector = match.group(3) + methodTypeCharacter = match.group('scope') + classNameOrExpression = match.group('target') + category = match.group('category') + selector = match.group('selector') methodIsClassMethod = (methodTypeCharacter == '+') @@ -135,7 +146,8 @@ def run(self, arguments, options): return breakpointClassName = objc.class_getName(nextClass) - breakpointFullName = '{}[{} {}]'.format(methodTypeCharacter, breakpointClassName, selector) + formattedCategory = category if category else '' + breakpointFullName = '{}[{}{} {}]'.format(methodTypeCharacter, breakpointClassName, formattedCategory, selector) breakpointCondition = None if targetIsClass: @@ -145,7 +157,11 @@ def run(self, arguments, options): print 'Setting a breakpoint at {} with condition {}'.format(breakpointFullName, breakpointCondition) - lldb.debugger.HandleCommand('breakpoint set --fullname "{}" --condition "{}"'.format(breakpointFullName, breakpointCondition)) + if category: + lldb.debugger.HandleCommand('breakpoint set --fullname "{}" --condition "{}"'.format(breakpointFullName, breakpointCondition)) + else: + breakpointPattern = '{}\[{}(\(.+\))? {}\]'.format(methodTypeCharacter, breakpointClassName, selector) + lldb.debugger.HandleCommand('breakpoint set --func-regex "{}" --condition "{}"'.format(breakpointPattern, breakpointCondition)) def classItselfImplementsSelector(klass, selector): thisMethod = objc.class_getInstanceMethod(klass, selector) diff --git a/commands/FBDisplayCommands.py b/commands/FBDisplayCommands.py index d4d2abc..bd2b6ea 100644 --- a/commands/FBDisplayCommands.py +++ b/commands/FBDisplayCommands.py @@ -46,10 +46,10 @@ def options(self): def run(self, args, options): colorClassName = 'UIColor' isMac = runtimeHelpers.isMacintoshArch() - + if isMac: colorClassName = 'NSColor' - + layer = viewHelpers.convertToLayer(args[0]) lldb.debugger.HandleCommand('expr (void)[%s setBorderWidth:(CGFloat)%s]' % (layer, options.width)) lldb.debugger.HandleCommand('expr (void)[%s setBorderColor:(CGColorRef)[(id)[%s %sColor] CGColor]]' % (layer, colorClassName, options.color)) diff --git a/commands/FBFindCommands.py b/commands/FBFindCommands.py index 9763c89..cbd05fe 100644 --- a/commands/FBFindCommands.py +++ b/commands/FBFindCommands.py @@ -98,7 +98,7 @@ def printMatchesInViewOutputStringAndCopyFirstToClipboard(needle, haystack): view = match.groups()[-1] className = fb.evaluateExpressionValue('(id)[(' + view + ') class]').GetObjectDescription() print('{} {}'.format(view, className)) - if first == None: + if first is None: first = view cmd = 'echo %s | tr -d "\n" | pbcopy' % view os.system(cmd) @@ -125,7 +125,7 @@ def run(self, arguments, options): if re.match(r'.*' + needle + '.*', a11yLabel, re.IGNORECASE): print('{} {}'.format(view, a11yLabel)) - if first == None: + if first is None: first = view cmd = 'echo %s | tr -d "\n" | pbcopy' % first os.system(cmd) diff --git a/commands/FBFlickerCommands.py b/commands/FBFlickerCommands.py index 0171c07..85b3913 100644 --- a/commands/FBFlickerCommands.py +++ b/commands/FBFlickerCommands.py @@ -110,10 +110,10 @@ def inputCallback(self, input): elif input == 'p': recusionName = 'recursiveDescription' isMac = runtimeHelpers.isMacintoshArch() - + if isMac: recursionName = '_subtreeDescription' - + lldb.debugger.HandleCommand('po [(id)' + oldView + ' ' + recusionName + ']') else: print '\nI really have no idea what you meant by \'' + input + '\'... =\\\n' diff --git a/commands/FBInvocationCommands.py b/commands/FBInvocationCommands.py index d0cce5a..a7448c6 100644 --- a/commands/FBInvocationCommands.py +++ b/commands/FBInvocationCommands.py @@ -89,7 +89,7 @@ def stackStartAddressInSelectedFrame(frame): return int(frame.EvaluateExpression('($esp + 8)').GetValue()) else: return int(frame.EvaluateExpression('($ebp + 8)').GetValue()) - + def findArgAtIndexFromStackFrame(frame, index): return fb.evaluateExpression('*(int *)' + str(findArgAdressAtIndexFromStackFrame(frame, index))) @@ -134,36 +134,36 @@ def prettyPrintInvocation(frame, invocation): print readableString else: if encoding[0] == '{': - encoding = encoding[1:len(encoding)-1] + encoding = encoding[1:] print (hex(address) + ', address of ' + encoding + ' ' + description).strip() index += 1 def argumentAsString(frame, address, encoding): if encoding[0] == '{': - encoding = encoding[1:len(encoding)-1] + encoding = encoding[1:] encodingMap = { - 'c' : 'char', - 'i' : 'int', - 's' : 'short', - 'l' : 'long', - 'q' : 'long long', - - 'C' : 'unsigned char', - 'I' : 'unsigned int', - 'S' : 'unsigned short', - 'L' : 'unsigned long', - 'Q' : 'unsigned long long', - - 'f' : 'float', - 'd' : 'double', - 'B' : 'bool', - 'v' : 'void', - '*' : 'char *', - '@' : 'id', - '#' : 'Class', - ':' : 'SEL', + 'c': 'char', + 'i': 'int', + 's': 'short', + 'l': 'long', + 'q': 'long long', + + 'C': 'unsigned char', + 'I': 'unsigned int', + 'S': 'unsigned short', + 'L': 'unsigned long', + 'Q': 'unsigned long long', + + 'f': 'float', + 'd': 'double', + 'B': 'bool', + 'v': 'void', + '*': 'char *', + '@': 'id', + '#': 'Class', + ':': 'SEL', } pointers = '' @@ -202,5 +202,5 @@ def argumentAsString(frame, address, encoding): description = value.GetSummary() if description: return type + ': ' + description - + return None diff --git a/commands/FBPrintCommands.py b/commands/FBPrintCommands.py index 02919bf..d371bc8 100644 --- a/commands/FBPrintCommands.py +++ b/commands/FBPrintCommands.py @@ -15,6 +15,7 @@ import fblldbviewcontrollerhelpers as vcHelpers import fblldbviewhelpers as viewHelpers import fblldbobjcruntimehelpers as runtimeHelpers +import fblldbobjecthelpers as objectHelpers def lldbcommands(): return [ @@ -28,6 +29,8 @@ def lldbcommands(): FBPrintOnscreenTableViewCells(), FBPrintInternals(), FBPrintInstanceVariable(), + FBPrintKeyPath(), + FBPrintAccessibilityTree(), ] class FBPrintViewHierarchyCommand(fb.FBCommand): @@ -49,10 +52,10 @@ def args(self): def run(self, arguments, options): maxDepth = int(options.depth) isMac = runtimeHelpers.isMacintoshArch() - - if (arguments[0] == '__keyWindow_dynamic__'): + + if arguments[0] == '__keyWindow_dynamic__': arguments[0] = '(id)[[UIApplication sharedApplication] keyWindow]' - + if isMac: arguments[0] = '(id)[[[[NSApplication sharedApplication] windows] objectAtIndex:0] contentView]' @@ -65,9 +68,9 @@ def run(self, arguments, options): print 'Failed to walk view hierarchy. Make sure you pass a view, not any other kind of object or expression.' else: printingMethod = 'recursiveDescription' - if (isMac): + if isMac: printingMethod = '_subtreeDescription' - + description = fb.evaluateExpressionValue('(id)[' + arguments[0] + ' ' + printingMethod + ']').GetObjectDescription() if maxDepth > 0: separator = re.escape(" | ") @@ -100,10 +103,14 @@ def args(self): def run(self, arguments, options): isMac = runtimeHelpers.isMacintoshArch() - - if (arguments[0] == '__keyWindow_rootVC_dynamic__'): + + if arguments[0] == '__keyWindow_rootVC_dynamic__': + if fb.evaluateBooleanExpression('[UIViewController respondsToSelector:@selector(_printHierarchy)]'): + lldb.debugger.HandleCommand('po [UIViewController _printHierarchy]') + return + arguments[0] = '(id)[(id)[[UIApplication sharedApplication] keyWindow] rootViewController]' - if (isMac): + if isMac: arguments[0] = '(id)[[[[NSApplication sharedApplication] windows] objectAtIndex:0] contentViewController]' print vcHelpers.viewControllerRecursiveDescription(arguments[0]) @@ -123,7 +130,7 @@ def run(self, arguments, options): def _printIterative(initialValue, generator): indent = 0 for currentValue in generator(initialValue): - print ' | '*indent + currentValue + print ' | ' * indent + currentValue indent += 1 @@ -267,8 +274,46 @@ def run(self, arguments, options): object = fb.evaluateObjectExpression(commandForObject) objectClass = fb.evaluateExpressionValue('(id)[(' + object + ') class]').GetObjectDescription() - ivarTypeCommand = '((char *)ivar_getTypeEncoding((Ivar)object_getInstanceVariable((id){}, \"{}\", 0)))[0]'.format(object, ivarName) + ivarTypeCommand = '((char *)ivar_getTypeEncoding((void*)object_getInstanceVariable((id){}, \"{}\", 0)))[0]'.format(object, ivarName) ivarTypeEncodingFirstChar = fb.evaluateExpression(ivarTypeCommand) printCommand = 'po' if ('@' in ivarTypeEncodingFirstChar) else 'p' lldb.debugger.HandleCommand('{} (({} *)({}))->{}'.format(printCommand, objectClass, object, ivarName)) + +class FBPrintKeyPath(fb.FBCommand): + def name(self): + return 'pkp' + + def description(self): + return "Print out the value of the key path expression using -valueForKeyPath:" + + def args(self): + return [ + fb.FBCommandArgument(arg='keypath', type='NSString *', help='The keypath to print'), + ] + + def run(self, arguments, options): + command = arguments[0] + if len(command.split('.')) == 1: + lldb.debugger.HandleCommand("po " + command) + else: + objectToMessage, keypath = command.split('.', 1) + object = fb.evaluateObjectExpression(objectToMessage) + printCommand = 'po [{} valueForKeyPath:@"{}"]'.format(object, keypath) + lldb.debugger.HandleCommand(printCommand) + +class FBPrintAccessibilityTree(fb.FBCommand): + def name(self): + return 'pae' + + def description(self): + return "Print out the accessibility heirarchy. Traverses the accessibilityElements if present and the accessibility inspector is enabled, otherwise the subviews." + + def args(self): + return [ + fb.FBCommandArgument(arg='object', type='id', default="[[UIApplication sharedApplication] keyWindow]", help='The object to print accessibility information for'), + ] + + def run(self, arguments, options): + object = fb.evaluateObjectExpression(arguments[0]) + return viewHelpers.accessibilityRecursiveDescription(object, "") diff --git a/commands/FBVisualizationCommands.py b/commands/FBVisualizationCommands.py index dd547fd..da90fe3 100755 --- a/commands/FBVisualizationCommands.py +++ b/commands/FBVisualizationCommands.py @@ -33,13 +33,13 @@ def _showImage(commandForImage): else: raise - imageDataAddress = fb.evaluateObjectExpression('UIImagePNGRepresentation((id)' + commandForImage +')') + imageDataAddress = fb.evaluateObjectExpression('UIImagePNGRepresentation((id)' + commandForImage + ')') imageBytesStartAddress = fb.evaluateExpression('(void *)[(id)' + imageDataAddress + ' bytes]') imageBytesLength = fb.evaluateExpression('(NSUInteger)[(id)' + imageDataAddress + ' length]') - address = int(imageBytesStartAddress,16) + address = int(imageBytesStartAddress, 16) length = int(imageBytesLength) - + if not (address or length): print 'Could not get image data.' return @@ -47,7 +47,7 @@ def _showImage(commandForImage): process = lldb.debugger.GetSelectedTarget().GetProcess() error = lldb.SBError() mem = process.ReadMemory(address, length, error) - + if error is not None and str(error) != 'success': print error else: @@ -76,35 +76,35 @@ def _dataIsImage(data): data = '(' + data + ')' frame = lldb.debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame() - result = frame.EvaluateExpression('(id)[UIImage imageWithData:' + data + ']'); + result = frame.EvaluateExpression('(id)[UIImage imageWithData:' + data + ']') if result.GetError() is not None and str(result.GetError()) != 'success': - return 0; + return 0 else: - isImage = result.GetValueAsUnsigned() != 0; + isImage = result.GetValueAsUnsigned() != 0 if isImage: - return 1; + return 1 else: - return 0; + return 0 def _dataIsString(data): data = '(' + data + ')' frame = lldb.debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame() - result = frame.EvaluateExpression('(NSString*)[[NSString alloc] initWithData:' + data + ' encoding:4]'); + result = frame.EvaluateExpression('(NSString*)[[NSString alloc] initWithData:' + data + ' encoding:4]') if result.GetError() is not None and str(result.GetError()) != 'success': - return 0; + return 0 else: - isString = result.GetValueAsUnsigned() != 0; + isString = result.GetValueAsUnsigned() != 0 if isString: - return 1; + return 1 else: - return 0; + return 0 def _visualize(target): target = '(' + target + ')' - + if fb.evaluateBooleanExpression('(unsigned long)CFGetTypeID((CFTypeRef)' + target + ') == (unsigned long)CGImageGetTypeID()'): _showImage('(id)[UIImage imageWithCGImage:' + target + ']') else: @@ -116,11 +116,11 @@ def _visualize(target): _showLayer(target) elif objectHelpers.isKindOfClass(target, 'NSData'): if _dataIsImage(target): - _showImage('(id)[UIImage imageWithData:' + target + ']'); + _showImage('(id)[UIImage imageWithData:' + target + ']') elif _dataIsString(target): lldb.debugger.HandleCommand('po (NSString*)[[NSString alloc] initWithData:' + target + ' encoding:4]') else: - print 'Data isn\'t an image and isn\'t a string.'; + print 'Data isn\'t an image and isn\'t a string.' else: print '{} isn\'t supported. You can visualize UIImage, CGImageRef, UIView, CALayer or NSData.'.format(objectHelpers.className(target)) diff --git a/fblldb.py b/fblldb.py index 1ea393b..383449b 100644 --- a/fblldb.py +++ b/fblldb.py @@ -64,7 +64,7 @@ def runCommand(debugger, input, result, dict): # thing. options = command.options() if len(options) == 0: - if not '--' in splitInput: + if '--' not in splitInput: splitInput.insert(0, '--') parser = optionParserForCommand(command) @@ -74,7 +74,7 @@ def runCommand(debugger, input, result, dict): # the initial args form an expression and combine them into a single arg. if len(args) > len(command.args()): overhead = len(args) - len(command.args()) - head = args[:overhead+1] # Take N+1 and reduce to 1. + head = args[:overhead + 1] # Take N+1 and reduce to 1. args = [' '.join(head)] + args[-overhead:] if validateArgsForCommand(args, command): @@ -145,8 +145,8 @@ def helpForCommand(command, filename): help += '; ' + option.help optionSyntax += ' [{name}{arg}]'.format( - name= option.longName or option.shortName, - arg = '' if option.boolean else ('=' + option.argName) + name=(option.longName or option.shortName), + arg=('' if option.boolean else ('=' + option.argName)) ) help += '\n\nSyntax: ' + command.name() + optionSyntax + argSyntax @@ -166,4 +166,3 @@ def usageForCommand(command): usage += ' ' + arg.argName return usage - diff --git a/fblldbbase.py b/fblldbbase.py index c2531ab..82eb4ab 100644 --- a/fblldbbase.py +++ b/fblldbbase.py @@ -35,8 +35,9 @@ def description(self): def run(self, arguments, option): pass +NSNOTFOUND32BIT = 0x7fffffff -def evaluateExpressionValue(expression, printErrors = True): +def evaluateExpressionValue(expression, printErrors=True): # lldb.frame is supposed to contain the right frame, but it doesnt :/ so do the dance frame = lldb.debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame() value = frame.EvaluateExpression(expression) @@ -44,19 +45,22 @@ def evaluateExpressionValue(expression, printErrors = True): print value.GetError() return value -def evaluateIntegerExpression(expression, printErrors = True): +def evaluateIntegerExpression(expression, printErrors=True): output = evaluateExpression('(int)(' + expression + ')', printErrors).replace('\'', '') + value = 0 if output.startswith('\\x'): # Booleans may display as \x01 (Hex) - output = output[2:] + value = int(output[2:], 16) elif output.startswith('\\'): # Or as \0 (Dec) - output = output[1:] - return int(output, 16) + value = int(output[1:], 10) + else: + value = int(output, 10) + return value -def evaluateBooleanExpression(expression, printErrors = True): +def evaluateBooleanExpression(expression, printErrors=True): return (int(evaluateIntegerExpression('(BOOL)(' + expression + ')', printErrors)) != 0) -def evaluateExpression(expression, printErrors = True): +def evaluateExpression(expression, printErrors=True): return evaluateExpressionValue(expression, printErrors).GetValue() -def evaluateObjectExpression(expression, printErrors = True): +def evaluateObjectExpression(expression, printErrors=True): return evaluateExpression('(id)(' + expression + ')', printErrors) diff --git a/fblldbinputhelpers.py b/fblldbinputhelpers.py index 5214b71..a4d7b2d 100644 --- a/fblldbinputhelpers.py +++ b/fblldbinputhelpers.py @@ -33,9 +33,9 @@ def stop(self): self.inputReader.SetIsDone(True) def handleInput(self, inputReader, notification, bytes): - if (notification == lldb.eInputReaderGotToken): + if notification == lldb.eInputReaderGotToken: self.callback(bytes) - elif (notification == lldb.eInputReaderInterrupt): + elif notification == lldb.eInputReaderInterrupt: self.stop() - + return len(bytes) diff --git a/fblldbobjcruntimehelpers.py b/fblldbobjcruntimehelpers.py index 1b205db..70562e1 100644 --- a/fblldbobjcruntimehelpers.py +++ b/fblldbobjcruntimehelpers.py @@ -42,7 +42,7 @@ def currentArch(): def functionPreambleExpressionForSelf(): import re - + arch = currentArch() expressionForSelf = None if arch == 'i386': @@ -57,22 +57,22 @@ def functionPreambleExpressionForSelf(): def functionPreambleExpressionForObjectParameterAtIndex(parameterIndex): import re - + arch = currentArch() expresssion = None if arch == 'i386': expresssion = '*(id*)($esp + ' + str(12 + parameterIndex * 4) + ')' elif arch == 'x86_64': - if (parameterIndex > 3): + if parameterIndex > 3: raise Exception("Current implementation can not return object at index greater than 3 for arc x86_64") registersList = ['rdx', 'rcx', 'r8', 'r9'] expresssion = '(id)$' + registersList[parameterIndex] elif arch == 'arm64': - if (parameterIndex > 5): - raise Exception("Current implementation can not return object at index greater than 5 for arm64") + if parameterIndex > 5: + raise Exception("Current implementation can not return object at index greater than 5 for arm64") expresssion = '(id)$x' + str(parameterIndex + 2) elif re.match(r'^armv.*$', arch): - if (parameterIndex > 3): + if parameterIndex > 3: raise Exception("Current implementation can not return object at index greater than 1 for arm32") expresssion = '(id)$r' + str(parameterIndex + 2) return expresssion @@ -81,8 +81,8 @@ def isMacintoshArch(): arch = currentArch() if not arch == 'x86_64': return False - - nsClassName ='NSApplication' + + nsClassName = 'NSApplication' command = '(void*)objc_getClass("{}")'.format(nsClassName) return (fb.evaluateBooleanExpression(command + '!= nil')) diff --git a/fblldbobjecthelpers.py b/fblldbobjecthelpers.py index 174ab3d..14bc009 100755 --- a/fblldbobjecthelpers.py +++ b/fblldbobjecthelpers.py @@ -15,4 +15,28 @@ def isKindOfClass(obj, className): return fb.evaluateBooleanExpression(isKindOfClassStr.format(className)) def className(obj): - return fb.evaluateExpressionValue('(id)[(' + obj + ') class]').GetObjectDescription() \ No newline at end of file + return fb.evaluateExpressionValue('(id)[(' + obj + ') class]').GetObjectDescription() + +def valueForKey(obj, key): + return fb.evaluateExpressionValue('(id)[%s valueForKey:@"%s"]' % (obj, key)).GetObjectDescription() + +def isNil(obj): + return obj == "" or obj == "" + +def displayValueForKey(obj, key): + value = valueForKey(obj, key) + return "{}='{}'".format(key, value) if not isNil(value) else "" + +def displayValueForKeys(obj, keys): + def displayValueForThisObjectKey(key): + return displayValueForKey(obj, key) + return " ".join(map(displayValueForThisObjectKey, keys)) + +def displayObjectWithString(obj, string): + return "<{}:{} {}>".format( + className(obj), + obj, + string) + +def displayObjectWithKeys(obj, keys): + return displayObjectWithString(obj, displayValueForKeys(obj, keys)) diff --git a/fblldbviewcontrollerhelpers.py b/fblldbviewcontrollerhelpers.py index 5b66d10..b38fa77 100644 --- a/fblldbviewcontrollerhelpers.py +++ b/fblldbviewcontrollerhelpers.py @@ -47,7 +47,7 @@ def _recursiveViewControllerDescriptionWithPrefixAndChildPrefix(vc, string, pref if isModal: modalVC = fb.evaluateObjectExpression('(id)[(id)%s presentedViewController]' % (vc)) - s += _recursiveViewControllerDescriptionWithPrefixAndChildPrefix(modalVC, string, childPrefix + ' *M' , nextPrefix) + s += _recursiveViewControllerDescriptionWithPrefixAndChildPrefix(modalVC, string, childPrefix + ' *M', nextPrefix) s += '\n// \'*M\' means the view controller is presented modally.' - return string + s \ No newline at end of file + return string + s diff --git a/fblldbviewhelpers.py b/fblldbviewhelpers.py index be2d41f..6a9d4f0 100644 --- a/fblldbviewhelpers.py +++ b/fblldbviewhelpers.py @@ -8,8 +8,9 @@ # of patent rights can be found in the PATENTS file in the same directory. import lldb - +import re import fblldbbase as fb +import fblldbobjecthelpers as objectHelpers def flushCoreAnimationTransaction(): lldb.debugger.HandleCommand('expr (void)[CATransaction flush]') @@ -58,11 +59,11 @@ def convertToLayer(viewOrLayer): def upwardsRecursiveDescription(view, maxDepth=0): if not fb.evaluateBooleanExpression('[(id)%s isKindOfClass:(Class)[UIView class]]' % view) and not fb.evaluateBooleanExpression('[(id)%s isKindOfClass:(Class)[NSView class]]' % view): return None - + currentView = view recursiveDescription = [] depth = 0 - + while currentView and (maxDepth <= 0 or depth <= maxDepth): depth += 1 @@ -73,17 +74,72 @@ def upwardsRecursiveDescription(view, maxDepth=0): currentView = None except: currentView = None - + if viewDescription: recursiveDescription.insert(0, viewDescription) if len(viewDescription) == 0: - return None - + return None + currentPrefix = "" builder = "" for viewDescription in recursiveDescription: builder += currentPrefix + viewDescription + "\n" currentPrefix += " | " - + return builder + +# GetObjectDescription will try to return the pointer. +# However on UIAccessibilityElements, the result will look like this: +# [UITableViewSectionElement]{0x79eb2280} section: 0 (isHeader: 1) +# [UITableViewCellAccessibilityElement - 0x79eac160] <..... +# So, just get the first hex address +def firstHexInDescription(object): + return re.findall(r'0x[0-9A-F]+', "{}".format(object), re.I)[0] + +def accessibilityDescription(object): + if isAccessibilityElement(object): + return objectHelpers.displayObjectWithKeys(object, ["accessibilityLabel", "accessibilityValue", "accessibilityHint"]) + else: + return objectHelpers.displayObjectWithString(object, "isAccessibilityElement=NO") + +def isAccessibilityElement(object): + return fb.evaluateBooleanExpression('[(id)%s isAccessibilityElement]' % object) + +def accessibilityElementAtIndex(object, index): + cmd = '(id)[%s accessibilityElementAtIndex:%s]' % (object, index) + obj = firstHexInDescription(fb.evaluateExpressionValue(cmd)) + return obj + +def accessibilityChildren(object): + accessibilityCount = fb.evaluateIntegerExpression("(int)[%s accessibilityElementCount]" % (object)) + aeChildren = [] + if accessibilityCount < fb.NSNOTFOUND32BIT: + for i in range(0, accessibilityCount): + aeChildren.append(accessibilityElementAtIndex(object, i)) + return aeChildren + +def subviews(view): + subviewResult = [] + responds = fb.evaluateBooleanExpression('[(id)%s respondsToSelector:(SEL)@selector(subviews)]' % view) + if responds: + subviews = fb.evaluateExpression('(id)[%s subviews]' % view) + subviewsCount = fb.evaluateIntegerExpression('[(id)%s count]' % subviews) + if subviewsCount > 0: + for i in range(0, subviewsCount): + subview = fb.evaluateExpression('(id)[%s objectAtIndex:%i]' % (subviews, i)) + subviewResult.append(subview) + return subviewResult + + +def accessibilityRecursiveDescription(object, prefix="", childType=""): + print '%s%s%s' % (prefix, childType, accessibilityDescription(object)) + nextPrefix = prefix + ' |' + aeChildren = accessibilityChildren(object) + for ae in aeChildren: + accessibilityRecursiveDescription(ae, nextPrefix, 'A ') + + if len(aeChildren) == 0: + for subview in subviews(object): + accessibilityRecursiveDescription(subview, nextPrefix, 'S ') +