From 246fb5e1a3a084b15c434c12f7626d17d888e7b0 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 23 Apr 2026 19:23:44 +0500 Subject: [PATCH] jvm: fix macOS accessibility crash and update Compose to 1.10.3 - Introduce `A11yCrashGuard` to prevent app freezes on macOS caused by a known `NullPointerException` in the Compose Multiplatform 1.10.x accessibility bridge. - Initialize `A11yCrashGuard` in `DesktopApp.kt` to catch and suppress the specific NPE within the AWT EventDispatchThread. - Upgrade Compose Multiplatform and related JetBrains dependencies from 1.10.1 to 1.10.3. - Expand allowed capabilities in `.claude/settings.local.json` to include web search and specific JetBrains/GitHub domains. --- .claude/settings.local.json | 8 ++- .../zed/rainxch/githubstore/A11yCrashGuard.kt | 66 +++++++++++++++++++ .../zed/rainxch/githubstore/DesktopApp.kt | 4 ++ gradle/libs.versions.toml | 6 +- 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 57ff41a5..ca85f979 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,13 @@ "allow": [ "Bash(grep -r \"\\\\.FLATPAK\\\\|Flatpak\\\\|flatpak\" . --include=*.kt)", "Bash(./gradlew *)", - "Bash(rtk find *)" + "Bash(rtk find *)", + "WebSearch", + "WebFetch(domain:github.com)", + "WebFetch(domain:www.jetbrains.com)", + "WebFetch(domain:youtrack.jetbrains.com)", + "WebFetch(domain:raw.githubusercontent.com)", + "WebFetch(domain:blog.jetbrains.com)" ] } } diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt new file mode 100644 index 00000000..42b56ca9 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt @@ -0,0 +1,66 @@ +package zed.rainxch.githubstore + +import java.awt.AWTEvent +import java.awt.EventQueue +import java.awt.Toolkit +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Workaround for a Compose Multiplatform 1.10.x NPE on macOS where the native + * AX bridge (`sun.lwawt.macosx.CAccessible$AXChangeNotifier`) queries a + * Compose semantic node that has already been removed by Compose's own + * accessibility sync loop. The stack trace fingerprint is: + * + * androidx.compose.ui.platform.a11y.SemanticsOwnerAccessibility.accessibleParentOf + * -> sun.lwawt.macosx.CAccessible$AXChangeNotifier.propertyChange + * + * The uncaught exception poisons the AWT EventDispatchThread and the app + * appears to freeze/crash on click. Installing a filtering [EventQueue] + * swallows only that specific NPE so the EDT keeps draining events. + * Trade-off: macOS VoiceOver may miss updates on those removed nodes. + * Remove once the upstream fix lands (track against Compose MP 1.11+). + * + * See [GitHub-Store#330](https://github.com/OpenHub-Store/GitHub-Store/issues/330). + */ +object A11yCrashGuard { + fun install() { + val osName = System.getProperty("os.name")?.lowercase().orEmpty() + if (!osName.contains("mac")) return + Toolkit.getDefaultToolkit().systemEventQueue.push(FilteringEventQueue()) + } + + private class FilteringEventQueue : EventQueue() { + private val warned = AtomicBoolean(false) + + override fun dispatchEvent(event: AWTEvent) { + try { + super.dispatchEvent(event) + } catch (npe: NullPointerException) { + if (isComposeA11yNpe(npe)) { + if (warned.compareAndSet(false, true)) { + System.err.println( + "[A11yCrashGuard] Suppressed Compose a11y NPE on macOS " + + "(known issue, see GitHub-Store#330). Further occurrences silenced.", + ) + } + return + } + throw npe + } + } + + private fun isComposeA11yNpe(throwable: Throwable): Boolean { + var current: Throwable? = throwable + while (current != null) { + if (current.stackTrace.any { frame -> + frame.className.startsWith("androidx.compose.ui.platform.a11y") + } + ) { + return true + } + current = current.cause + } + return false + } + } +} diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt index 0c4e6a91..b89a9469 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt @@ -37,6 +37,10 @@ fun main(args: Array) { // `CrashReporter.resolveLogDir` for the per-OS path). CrashReporter.install() + // Guard the AWT EventDispatchThread against a known Compose MP 1.10.x NPE + // raised by the macOS accessibility bridge (see `A11yCrashGuard`). + A11yCrashGuard.install() + // Reduce JVM DNS cache TTL so network changes (VPN on/off) are picked up quickly. // Default JVM caches positive lookups for 30s and negative lookups forever, // which breaks connectivity when a VPN changes DNS/routing mid-session. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7bc22e7f..e4e37a45 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ androidTools = "32.0.1" # Compose compose-hot-reload = "1.0.0" -compose-multiplatform = "1.10.1" +compose-multiplatform = "1.10.3" compose-lifecycle = "2.9.6" navigation-compose = "2.9.2" jetbrains-core-bundle = "1.0.1" @@ -33,7 +33,7 @@ room = "2.8.4" slf4jSimple = "2.0.17" sqlite = "2.6.2" datastore = "1.2.0" -compose-jetbrains = "1.10.1" +compose-jetbrains = "1.10.3" compose-jetbrains-material-you = "1.11.0-alpha03" jetbrains-savedstate = "1.4.0" moko = "0.20.1" @@ -45,7 +45,7 @@ shizuku = "13.1.5" hidden-api = "4.4.0" jsystemthemedetector = "3.9.1" work = "2.11.1" -resources="1.10.1" +resources="1.10.3" ktlint-gradle = "12.1.1" flatpak-gradle-generator = "1.7.0"