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:
dan 2026-01-15 15:39:05 -08:00
parent ec6c81b436
commit 5f5675d1ca
5 changed files with 87 additions and 30 deletions

View file

@ -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

View file

@ -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:")

View file

@ -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:")

View file

@ -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:

View file

@ -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:")