diff --git a/README.md b/README.md index 9672658..da294da 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [[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/). +For a comprehensive overview of LLDB, and how Chisel complements 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/FBAccessibilityCommands.py b/commands/FBAccessibilityCommands.py new file mode 100644 index 0000000..f71543c --- /dev/null +++ b/commands/FBAccessibilityCommands.py @@ -0,0 +1,106 @@ +#!/usr/bin/python + +# Copyright (c) 2015, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. + +import re +import os + +import lldb +import fblldbbase as fb +import fblldbobjecthelpers as objHelpers + +# This is the key corresponding to accessibility label in _accessibilityElementsInContainer: +ACCESSIBILITY_LABEL_KEY = 2001 + +def lldbcommands(): + return [ + FBPrintAccessibilityLabels(), + FBFindViewByAccessibilityLabelCommand(), + ] + +class FBPrintAccessibilityLabels(fb.FBCommand): + def name(self): + return 'pa11y' + + def description(self): + return 'Print accessibility labels of all views in hierarchy of ' + + def args(self): + return [ fb.FBCommandArgument(arg='aView', type='UIView*', help='The view to print the hierarchy of.', default='(id)[[UIApplication sharedApplication] keyWindow]') ] + + def run(self, arguments, options): + forceStartAccessibilityServer(); + printAccessibilityHierarchy(arguments[0]) + +class FBFindViewByAccessibilityLabelCommand(fb.FBCommand): + def name(self): + return 'fa11y' + + def description(self): + return 'Find the views whose accessibility labels match labelRegex and puts the address of the first result on the clipboard.' + + def args(self): + return [ fb.FBCommandArgument(arg='labelRegex', type='string', help='The accessibility label regex to search the view hierarchy for.') ] + + def accessibilityGrepHierarchy(self, view, needle): + a11yLabel = accessibilityLabel(view) + #if we don't have any accessibility string - we should have some children + if int(a11yLabel.GetValue(), 16) == 0: + #We call private method that gives back all visible accessibility children for view + accessibilityElements = fb.evaluateObjectExpression('[[[UIApplication sharedApplication] keyWindow] _accessibilityElementsInContainer:0 topLevel:%s includeKB:0]' % view) + accessibilityElementsCount = fb.evaluateIntegerExpression('[%s count]' % accessibilityElements) + for index in range(0, accessibilityElementsCount): + subview = fb.evaluateObjectExpression('[%s objectAtIndex:%i]' % (accessibilityElements, index)) + self.accessibilityGrepHierarchy(subview, needle) + elif re.match(r'.*' + needle + '.*', a11yLabel.GetObjectDescription(), re.IGNORECASE): + classDesc = objHelpers.className(view) + print('({} {}) {}'.format(classDesc, view, a11yLabel.GetObjectDescription())) + + #First element that is found is copied to clipboard + if not self.foundElement: + self.foundElement = True + cmd = 'echo %s | tr -d "\n" | pbcopy' % view + os.system(cmd) + + def run(self, arguments, options): + forceStartAccessibilityServer() + rootView = fb.evaluateObjectExpression('[[UIApplication sharedApplication] keyWindow]') + self.foundElement = False + self.accessibilityGrepHierarchy(rootView, arguments[0]) + +def forceStartAccessibilityServer(): + #We try to start accessibility server only if we don't have needed method active + if not fb.evaluateBooleanExpression('[UIView instancesRespondToSelector:@selector(_accessibilityElementsInContainer:)]'): + #Starting accessibility server is different for simulator and device + if fb.evaluateExpressionValue('(id)[[UIDevice currentDevice] model]').GetObjectDescription().lower().find('simulator') >= 0: + lldb.debugger.HandleCommand('expr (void)[[UIApplication sharedApplication] accessibilityActivate]') + else: + lldb.debugger.HandleCommand('expr (void)[[[UIApplication sharedApplication] _accessibilityBundlePrincipalClass] _accessibilityStartServer]') + +def accessibilityLabel(view): + #using Apple private API to get real value of accessibility string for element. + return fb.evaluateExpressionValue('(id)[%s accessibilityAttributeValue:%i]' % (view, ACCESSIBILITY_LABEL_KEY), False) + +def printAccessibilityHierarchy(view, indent = 0): + a11yLabel = accessibilityLabel(view) + classDesc = objHelpers.className(view) + indentString = ' | ' * indent + + #if we don't have any accessibility string - we should have some children + if int(a11yLabel.GetValue(), 16) == 0: + print indentString + ('{} {}'.format(classDesc, view)) + #We call private method that gives back all visible accessibility children for view + accessibilityElements = fb.evaluateObjectExpression('[[[UIApplication sharedApplication] keyWindow] _accessibilityElementsInContainer:0 topLevel:%s includeKB:0]' % view) + accessibilityElementsCount = int(fb.evaluateExpression('(int)[%s count]' % accessibilityElements)) + for index in range(0, accessibilityElementsCount): + subview = fb.evaluateObjectExpression('[%s objectAtIndex:%i]' % (accessibilityElements, index)) + printAccessibilityHierarchy(subview, indent + 1) + else: + print indentString + ('({} {}) {}'.format(classDesc, view, a11yLabel.GetObjectDescription())) + + diff --git a/commands/FBFindCommands.py b/commands/FBFindCommands.py index cbd05fe..f2b7fda 100644 --- a/commands/FBFindCommands.py +++ b/commands/FBFindCommands.py @@ -19,7 +19,6 @@ def lldbcommands(): return [ FBFindViewControllerCommand(), FBFindViewCommand(), - FBFindViewByAccessibilityLabelCommand(), FBTapLoggerCommand(), ] @@ -104,33 +103,6 @@ def printMatchesInViewOutputStringAndCopyFirstToClipboard(needle, haystack): os.system(cmd) -class FBFindViewByAccessibilityLabelCommand(fb.FBCommand): - def name(self): - return 'fa11y' - - def description(self): - return 'Find the views whose accessibility labels match labelRegex and puts the address of the first result on the clipboard.' - - def args(self): - return [ fb.FBCommandArgument(arg='labelRegex', type='string', help='The accessibility label regex to search the view hierarchy for.') ] - - def run(self, arguments, options): - first = None - haystack = fb.evaluateExpressionValue('(id)[[[UIApplication sharedApplication] keyWindow] recursiveDescription]').GetObjectDescription() - needle = arguments[0] - - allViews = re.findall('.* (0x[0-9a-fA-F]*);.*', haystack) - for view in allViews: - a11yLabel = fb.evaluateExpressionValue('(id)[(' + view + ') accessibilityLabel]').GetObjectDescription() - if re.match(r'.*' + needle + '.*', a11yLabel, re.IGNORECASE): - print('{} {}'.format(view, a11yLabel)) - - if first is None: - first = view - cmd = 'echo %s | tr -d "\n" | pbcopy' % first - os.system(cmd) - - class FBTapLoggerCommand(fb.FBCommand): def name(self): return 'taplog' diff --git a/fblldbbase.py b/fblldbbase.py index db49365..c85cb02 100644 --- a/fblldbbase.py +++ b/fblldbbase.py @@ -36,14 +36,27 @@ def run(self, arguments, option): pass -def evaluateExpressionValue(expression, printErrors=True): +def evaluateExpressionValueWithLanguage(expression, language, printErrors): # 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) + expr_options = lldb.SBExpressionOptions() + expr_options.SetLanguage(language) # requires lldb r210874 (2014-06-13) / Xcode 6 + value = frame.EvaluateExpression(expression, expr_options) if printErrors and value.GetError() is not None and str(value.GetError()) != 'success': print value.GetError() return value +def evaluateExpressionValueInFrameLanguage(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() + language = frame.GetCompileUnit().GetLanguage() # requires lldb r222189 (2014-11-17) + return evaluateExpressionValueWithLanguage(expression, language, printErrors) + +# evaluates expression in Objective-C++ context, so it will work even for +# Swift projects +def evaluateExpressionValue(expression, printErrors=True): + return evaluateExpressionValueWithLanguage(expression, lldb.eLanguageTypeObjC_plus_plus, printErrors) + def evaluateIntegerExpression(expression, printErrors=True): output = evaluateExpression('(int)(' + expression + ')', printErrors).replace('\'', '') if output.startswith('\\x'): # Booleans may display as \x01 (Hex)