refactor(ui-query): improve exception handling with --debug flag
Add opt-in debug logging to all scripts: - set_debug() and log_debug() in common.py - --debug flag in all 4 scripts - Exception handlers now log context via log_debug() Keeps broad exception catching (needed for AT-SPI stale objects) but adds visibility when debugging. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ec6c81b436
commit
5f5675d1ca
|
|
@ -1,7 +1,38 @@
|
|||
"""Shared utilities for ui-query scripts."""
|
||||
|
||||
import sys
|
||||
|
||||
import pyatspi
|
||||
|
||||
# Debug mode - set via set_debug() or --debug flag in scripts
|
||||
_debug = False
|
||||
|
||||
|
||||
def set_debug(enabled):
|
||||
"""Enable or disable debug mode for verbose error logging."""
|
||||
global _debug
|
||||
_debug = enabled
|
||||
|
||||
|
||||
def log_debug(msg):
|
||||
"""Log message to stderr if debug mode is enabled."""
|
||||
if _debug:
|
||||
print(f"[DEBUG] {msg}", file=sys.stderr)
|
||||
|
||||
|
||||
def safe_get_attr(accessible, attr, default=""):
|
||||
"""Safely get an attribute from an accessible, handling stale objects.
|
||||
|
||||
AT-SPI objects can become stale at any time (window closed, app crashed).
|
||||
This helper catches those errors and returns a default value.
|
||||
"""
|
||||
try:
|
||||
value = getattr(accessible, attr)
|
||||
return value if value is not None else default
|
||||
except Exception as e:
|
||||
log_debug(f"Failed to get {attr}: {e}")
|
||||
return default
|
||||
|
||||
|
||||
def find_windows(pattern=None, app_name=None, all_windows=False):
|
||||
"""Find windows matching criteria.
|
||||
|
|
@ -113,10 +144,11 @@ def find_elements(accessible, role=None, name=None, results=None, max_depth=15,
|
|||
child = accessible.getChildAtIndex(i)
|
||||
if child:
|
||||
find_elements(child, role, name, results, max_depth, depth + 1, limit)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_debug(f"Error accessing child {i}: {e}")
|
||||
continue
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
log_debug(f"Error searching element: {e}")
|
||||
|
||||
return results
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import sys
|
|||
|
||||
import pyatspi
|
||||
|
||||
from common import find_windows, get_all_windows, find_elements
|
||||
from common import find_windows, get_all_windows, find_elements, set_debug, log_debug
|
||||
|
||||
|
||||
def get_element_info(element):
|
||||
|
|
@ -56,8 +56,8 @@ def get_element_info(element):
|
|||
"width": rect.width,
|
||||
"height": rect.height,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
log_debug(f"Failed to get geometry for {info['name']}: {e}")
|
||||
|
||||
# Get text content if available
|
||||
try:
|
||||
|
|
@ -66,8 +66,8 @@ def get_element_info(element):
|
|||
text = text_iface.getText(0, min(100, text_iface.characterCount))
|
||||
if text:
|
||||
info["text"] = text
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
log_debug(f"Failed to get text for {info['name']}: {e}")
|
||||
|
||||
# Build path for context
|
||||
path = []
|
||||
|
|
@ -123,8 +123,13 @@ def main():
|
|||
parser.add_argument("--app", "-a", help="Limit to application")
|
||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
parser.add_argument("--limit", type=int, default=20, help="Max results")
|
||||
parser.add_argument("--debug", action="store_true",
|
||||
help="Show debug messages for AT-SPI errors")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.debug:
|
||||
set_debug(True)
|
||||
|
||||
if not args.role and not args.name:
|
||||
parser.print_help()
|
||||
print("\nExamples:")
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from dataclasses import dataclass, field
|
|||
|
||||
import pyatspi
|
||||
|
||||
from common import find_windows
|
||||
from common import find_windows, set_debug, log_debug
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -52,8 +52,8 @@ def get_text_content(accessible):
|
|||
char_count = text_iface.characterCount
|
||||
if char_count > 0:
|
||||
text = text_iface.getText(0, char_count)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
log_debug(f"Failed to get text content: {e}")
|
||||
|
||||
return text
|
||||
|
||||
|
|
@ -82,7 +82,8 @@ def extract_text_tree(accessible, max_depth=10, depth=0):
|
|||
child_node = extract_text_tree(child, max_depth, depth + 1)
|
||||
if child_node:
|
||||
node.children.append(child_node)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_debug(f"Error accessing child {i} at depth {depth}: {e}")
|
||||
continue
|
||||
|
||||
# Prune nodes with no useful content
|
||||
|
|
@ -91,7 +92,8 @@ def extract_text_tree(accessible, max_depth=10, depth=0):
|
|||
|
||||
return node
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_debug(f"Error extracting text tree at depth {depth}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -127,8 +129,13 @@ def main():
|
|||
parser.add_argument("--all", action="store_true", help="Extract from all windows")
|
||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
parser.add_argument("--max-depth", type=int, default=10, help="Max tree depth")
|
||||
parser.add_argument("--debug", action="store_true",
|
||||
help="Show debug messages for AT-SPI errors")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.debug:
|
||||
set_debug(True)
|
||||
|
||||
if not args.pattern and not args.app and not args.all:
|
||||
parser.print_help()
|
||||
print("\nExamples:")
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import sys
|
|||
|
||||
import pyatspi
|
||||
|
||||
from common import set_debug, log_debug
|
||||
|
||||
|
||||
def get_window_info(window, verbose=False):
|
||||
"""Extract info from a window accessible."""
|
||||
|
|
@ -42,22 +44,23 @@ def get_window_info(window, verbose=False):
|
|||
"width": rect.width,
|
||||
"height": rect.height,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
log_debug(f"Failed to get geometry for {info['name']}: {e}")
|
||||
|
||||
if verbose:
|
||||
# Count children recursively (expensive)
|
||||
def count_children(acc):
|
||||
def count_children(acc, path=""):
|
||||
try:
|
||||
count = acc.childCount
|
||||
for i in range(acc.childCount):
|
||||
child = acc.getChildAtIndex(i)
|
||||
if child:
|
||||
count += count_children(child)
|
||||
count += count_children(child, f"{path}/{i}")
|
||||
return count
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
log_debug(f"Error counting children at {path}: {e}")
|
||||
return 0
|
||||
info["child_count"] = count_children(window)
|
||||
info["child_count"] = count_children(window, info["name"])
|
||||
|
||||
return info
|
||||
|
||||
|
|
@ -122,8 +125,13 @@ def main():
|
|||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
parser.add_argument("--verbose", "-v", action="store_true",
|
||||
help="Include child element counts (slower)")
|
||||
parser.add_argument("--debug", action="store_true",
|
||||
help="Show debug messages for AT-SPI errors")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.debug:
|
||||
set_debug(True)
|
||||
|
||||
try:
|
||||
apps = list_windows(verbose=args.verbose)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import sys
|
|||
|
||||
import pyatspi
|
||||
|
||||
from common import find_elements
|
||||
from common import find_elements, set_debug, log_debug
|
||||
|
||||
# Human-readable state descriptions
|
||||
STATE_DESCRIPTIONS = {
|
||||
|
|
@ -108,8 +108,8 @@ def get_element_details(element, all_states=False):
|
|||
"x": rect.x, "y": rect.y,
|
||||
"width": rect.width, "height": rect.height,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
log_debug(f"Failed to get geometry: {e}")
|
||||
|
||||
# Text content
|
||||
try:
|
||||
|
|
@ -120,8 +120,8 @@ def get_element_details(element, all_states=False):
|
|||
# Caret position if editable
|
||||
if states.get("editable"):
|
||||
info["caret_offset"] = text_iface.caretOffset
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
log_debug(f"Failed to get text: {e}")
|
||||
|
||||
# Value (for sliders, progress bars, etc.)
|
||||
try:
|
||||
|
|
@ -132,8 +132,8 @@ def get_element_details(element, all_states=False):
|
|||
"min": value_iface.minimumValue,
|
||||
"max": value_iface.maximumValue,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
log_debug(f"Failed to get value: {e}")
|
||||
|
||||
# Actions available
|
||||
try:
|
||||
|
|
@ -144,8 +144,8 @@ def get_element_details(element, all_states=False):
|
|||
actions.append(action_iface.getName(i))
|
||||
if actions:
|
||||
info["actions"] = actions
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
log_debug(f"Failed to get actions: {e}")
|
||||
|
||||
return info
|
||||
|
||||
|
|
@ -166,8 +166,8 @@ def find_focused_element(accessible, depth=0, max_depth=20):
|
|||
result = find_focused_element(child, depth + 1, max_depth)
|
||||
if result:
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
log_debug(f"Error finding focused element at depth {depth}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -217,8 +217,13 @@ def main():
|
|||
parser.add_argument("--focused", "-f", action="store_true", help="Show focused element only")
|
||||
parser.add_argument("--all-states", "-a", action="store_true", help="Show all state flags")
|
||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
parser.add_argument("--debug", action="store_true",
|
||||
help="Show debug messages for AT-SPI errors")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.debug:
|
||||
set_debug(True)
|
||||
|
||||
if not args.role and not args.name and not args.focused:
|
||||
parser.print_help()
|
||||
print("\nExamples:")
|
||||
|
|
|
|||
Loading…
Reference in a new issue