fix(macos): survive non-string AX titles and stop leaking AXObserver - #145
Conversation
Accessibility can return non-NSString values for kAXTitleAttribute. Bridging those with `as? String` calls `-[obj length]` and aborts the helper (NSHTTPURLResponse in ActivityWatch#144). Read titles via CF type checks. Also tear down the previous AXObserver properly: unregister the title and focused-window notifications, drop the run-loop source, and nil the observer before AXObserverCreate. Overwriting the CF out-pointer leaked Mach receive ports and wired memory (ActivityWatch#139). If the helper still dies, propagate its signal/exit status so aw-tauri restarts window tracking instead of treating the crash as a clean shutdown. Git-Session-Id: 03164cc9-6bb0-5435-925f-f425e310dded
|
@greptileai review |
|
@TimeToBuildBob review |
TimeToBuildBob
left a comment
There was a problem hiding this comment.
Reviewed against #144, #139 (Gassandrid's measured teardown), and aw-tauri's module manager. I authored this PR; this is the adversarial pass.
Verdict: land it. No blocking issues. GitHub will not let me approve my own PR.
Crash path (#144)
as? String on an AX value really does go through String._unconditionallyBridgeFromObjectiveC → -[obj length] with no class check. NSHTTPURLResponse is exactly that trap. axString / axElement via CFGetTypeID is the right fix, and every AX title/role/focused-window read on this path is converted.
The leftover urlRef as? NSURL is a different bridge (class-checked) and is the URL success path; axString is only the CFString fallback.
Leak path (#139)
tearDownObserver matches the patch Gassandrid measured at 200 and 2,000 app-switch iterations (no Mach-port growth):
- Unregister
kAXTitleChangedNotificationonoldWindow - Unregister
kAXFocusedWindowChangedNotificationonobservedApp - Drop the run-loop source
- Nil the refs so Swift ARC releases the
AXObserver(the Mach-port owner) - Create into a fresh out-pointer, not
&observer
Two extras vs that patch, both correct: deinit also tears down, and pollActiveWindow no longer force-unwraps a nil observer. RunLoop.current for both add and remove is consistent — mixing main/current (as the #139 sketch did) would leak if those ever diverged.
Exit status
swift_helper_exit_status(-6) == 134 is the mapping Popen.wait() uses for SIGABRT. aw-tauri restarts only when !output.status.success() (manager.rs); exit 0 was the silent-off. stop_module sets modules_pending_shutdown before SIGTERM, so mapping helper SIGTERM → 143 will not restart during quit. aw-qt already treats any death-while-started as unexpected (dialog, no auto-restart) — this change is for the tauri path.
Tests cover the mapping and the swift-strategy SystemExit(134) path.
CI
macos-latest already compiled this:
swiftc -target arm64-apple-macosx12.0 aw_watcher_window/macos.swift
Succeeded. The two warnings (optional interpolation in the Chrome/Safari title-diff logs) are pre-existing. The unchecked make build-swift box in the PR body is stale.
Out of scope
- #141 also rewrites observer lifecycle and will conflict. Land this first: it actually type-checks AX strings and unregisters the app-level focused-window notification; #141 does neither, and still has
windowTitle as? String. - Chrome/Safari ScriptingBridge failure still
returns without a heartbeat. That's #141, not this PR. - Remaining live check I cannot do here: Mach-port count under rapid app switching (
sudo lsmp). Gassandrid already ran that harness against this teardown; CI compiled the helper but did not rerun it.
No code changes requested.
Summary
macOS window tracking dies in two related ways after a few days:
windowTitleChangedbridgedkAXTitleAttributewithas? String. That path calls-[obj length]without a class check, so a non-NSString (hereNSHTTPURLResponse) aborts the helper withNSInvalidArgumentException.focusedAppChangedremoved the run-loop source and then letAXObserverCreateoverwrite the previous observer pointer withoutCFRelease. The old Mach receive port kept filling toqlimit(1024). Independently,focusedWindowChangedunregisteredkAXFocusedWindowChangedNotificationfrom the old window even though it had registeredkAXTitleChangedNotification.Changes
axString/axElement) instead of Swift's NSString bridge.-6→ 134) to a non-zero wrapper exit so the module manager restarts the watcher.Observer teardown follows the patch Gassandrid measured on #139 (200/2000 app-switch iterations, no port growth). I cannot compile the Swift helper on this Linux host; Python tests cover the exit-status path.
Related
Test plan
poetry run python -m pytest tests/test_main.py(19 passed)poetry run mypy aw_watcher_window/ --ignore-missing-importsmake build-swiftaw-watcher-window-macosMach port count stays bounded (sudo lsmp/sudo zprint)aw-watcher-windowexit non-zero so aw-tauri restarts it