From 26c6b3172b9ecba4f8a86d22ff24e840c1582483 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:36:32 +0000 Subject: [PATCH] desktop: start when JNA cannot load, turn off the macOS theme detector log (#7919) * desktop: do not fail startup when the system theme detector cannot load JNA The detector was created in top-level vals of Resources.desktop.kt and Theme.desktop.kt. When JNA's native library cannot be mapped (for example a noexec JNA temp directory), the LinkageError ended the app before any window opened. The detector is now one shared value that is null in that case, and the theme falls back to light. * desktop: turn off the macOS theme detector's log When macOS leaves the AppleInterfaceStyle default unset (light mode), the library's isDark() throws a NullPointerException inside the library, logs it as an error with a stack trace and returns false. slf4j-simple can only set a level per logger, so the whole com.jthemedetecor.MacOSThemeDetector logger is turned off. --- .../common/platform/Resources.desktop.kt | 8 +++-- .../simplex/common/ui/theme/Theme.desktop.kt | 7 +++-- .../resources/simplelogger.properties | 1 + apps/multiplatform/spec/services/theme.md | 8 +++-- ...-10-28-desktop-macos-theme-detector-log.md | 23 ++++++++++++++ .../2026-10-28-desktop-startup-without-jna.md | 30 +++++++++++++++++++ 6 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 apps/multiplatform/common/src/desktopMain/resources/simplelogger.properties create mode 100644 plans/2026-10-28-desktop-macos-theme-detector-log.md create mode 100644 plans/2026-10-28-desktop-startup-without-jna.md diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt index 062f26cfd7..24958500a3 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt @@ -26,9 +26,13 @@ actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle) actual fun StringResource.localized(): String = desc().toString() -private val detector: OsThemeDetector = OsThemeDetector.getDetector() +internal val systemThemeDetector: OsThemeDetector? = try { + OsThemeDetector.getDetector() +} catch (_: LinkageError) { + null +} actual fun isInNightMode() = try { - detector.isDark + systemThemeDetector?.isDark ?: false } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt index 8006b4febb..cb72aa660e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt @@ -4,10 +4,11 @@ import androidx.compose.runtime.Composable import chat.simplex.common.platform.Log import chat.simplex.common.platform.TAG import chat.simplex.common.platform.desktopPlatform +import chat.simplex.common.platform.systemThemeDetector import com.jthemedetecor.OsThemeDetector -private val detector: OsThemeDetector = OsThemeDetector.getDetector() - .apply { +private val detector: OsThemeDetector? = systemThemeDetector + ?.apply { registerListener(::reactOnDarkThemeChanges) if (desktopPlatform.isLinux()) Runtime.getRuntime().addShutdownHook(Thread(::stopGsettingsMonitor)) } @@ -23,7 +24,7 @@ private fun stopGsettingsMonitor() = @Composable actual fun isSystemInDarkTheme(): Boolean = try { - detector.isDark + detector?.isDark ?: false } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) diff --git a/apps/multiplatform/common/src/desktopMain/resources/simplelogger.properties b/apps/multiplatform/common/src/desktopMain/resources/simplelogger.properties new file mode 100644 index 0000000000..ea9eb5a00f --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/resources/simplelogger.properties @@ -0,0 +1 @@ +org.slf4j.simpleLogger.log.com.jthemedetecor.MacOSThemeDetector=off diff --git a/apps/multiplatform/spec/services/theme.md b/apps/multiplatform/spec/services/theme.md index e278dbe2a9..c223032285 100644 --- a/apps/multiplatform/spec/services/theme.md +++ b/apps/multiplatform/spec/services/theme.md @@ -408,12 +408,12 @@ Delegates to the standard Compose function which reads `Configuration.uiMode`. **Desktop** ([`Theme.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt)): ```kotlin -private val detector: OsThemeDetector = OsThemeDetector.getDetector() - .apply { registerListener(::reactOnDarkThemeChanges) } +private val detector: OsThemeDetector? = systemThemeDetector + ?.apply { registerListener(::reactOnDarkThemeChanges) } @Composable actual fun isSystemInDarkTheme(): Boolean = try { - detector.isDark + detector?.isDark ?: false } catch (e: Exception) { false // Fallback for macOS exceptions } @@ -421,6 +421,8 @@ actual fun isSystemInDarkTheme(): Boolean = try { Uses the [jSystemThemeDetector](https://github.com/Dansoftowner/jSystemThemeDetector) library (`OsThemeDetector`). The detector also registers a listener that calls `reactOnDarkThemeChanges()` proactively when the OS theme changes, ensuring the app responds even outside of composition. +`systemThemeDetector` is declared in `Resources.desktop.kt` as `OsThemeDetector.getDetector()` guarded by `catch (_: LinkageError)`. It is `null` when JNA's native library cannot be loaded; the app then starts with the light theme, `isInNightMode()` and `isSystemInDarkTheme()` return `false`, and no listener is registered. + ### reactOnDarkThemeChanges [`Theme.kt` line 763](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L763): diff --git a/plans/2026-10-28-desktop-macos-theme-detector-log.md b/plans/2026-10-28-desktop-macos-theme-detector-log.md new file mode 100644 index 0000000000..bcd613d77e --- /dev/null +++ b/plans/2026-10-28-desktop-macos-theme-detector-log.md @@ -0,0 +1,23 @@ +# Desktop: macOS theme detector logs an error in light mode + +## Problem + +On macOS the app prints `ERROR com.jthemedetecor.MacOSThemeDetector - Couldn't execute theme name query with the Os` with a stack trace. + +## Cause + +`MacOSThemeDetector.isDark()` reads `AppleInterfaceStyle`, which macOS leaves unset in light mode. `toStringViaUTF8` then returns `null`, `isDarkTheme(null)` throws `NullPointerException`, and the library logs it through slf4j-simple and returns `false`. The library catches the exception itself, so the app's `catch (e: Exception)` is never reached. Read from the jSystemThemeDetector 3.8 and jfa 1.2.0 bytecode; the unset key in light mode is not verified on a Mac. + +## Fix + +`simplelogger.properties` turns off the `com.jthemedetecor.MacOSThemeDetector` logger. This also hides that logger's other error ("Observer method cannot be added") and any other `RuntimeException` that `isDark()` catches; `isDark()` returns `false` as before. The oshi and Gnome loggers still print. + +## Approaches considered + +1. Turn off the logger (chosen): slf4j-simple can only set a level per logger, so this is the smallest change. +2. Read `AppleInterfaceStyle` in the app instead of the library: removes the cause, but is larger and cannot be tested here. +3. Bump jSystemThemeDetector to 3.9.1, which adds the null check: a different change. The same release rewrites the Gnome detector (it also watches `color-scheme`, and its loop does not block when `readLine` returns `null`, so it busy-loops once the `gsettings` process exits), which would need its own Gnome and shutdown-hook testing. + +## Scope + +Desktop only (`desktopMain`); in effect on macOS only. diff --git a/plans/2026-10-28-desktop-startup-without-jna.md b/plans/2026-10-28-desktop-startup-without-jna.md new file mode 100644 index 0000000000..675224b7d7 --- /dev/null +++ b/plans/2026-10-28-desktop-startup-without-jna.md @@ -0,0 +1,30 @@ +# Desktop starts when JNA cannot load + +## Problem + +On desktop the app exits before any window opens when the JNA native library cannot be loaded, for example when the JNA temp directory (`~/.cache/JNA/temp`) is on a `noexec` mount. The log shows `NoClassDefFoundError: Could not initialize class com.sun.jna.Native` from `Resources_desktopKt.`, caused by `UnsatisfiedLinkError: ... failed to map segment from shared object`. System theme detection is optional, yet it ends the startup. + +## Cause + +- `Resources.desktop.kt` (bf91666f0, #4109) and `Theme.desktop.kt` (26a233ab1, #2755) each create the detector in a top-level `val` (`OsThemeDetector.getDetector()`). +- `getDetector()` loads oshi, which loads JNA. The failure is a `LinkageError` thrown from a static initializer, outside the `try` around `isDark`, so nothing catches it. +- `Resources.desktop.kt` also declares `settings`, so the first preference read (`AppPreferences`) triggers the failing initializer. + +## Fix + +- `systemThemeDetector` in `Resources.desktop.kt` replaces the private `detector`: `getDetector()` inside `try`, `catch (_: LinkageError)` returns `null`. `WindowActivation.kt` catches `LinkageError` for the same condition (#7605). +- It stays an eager top-level `val`, so the detector is created at the same point as before. It does not log: `Log.w` reads `appPrefs`, and `AppPreferences` is under construction when `Resources_desktopKt` initialises (the first `settings` read), so logging there re-enters `appPrefs` and fails with a `NullPointerException`. On Linux oshi has already logged the failure; on Windows and macOS nothing is logged (not verified). +- `isInNightMode()` and `isSystemInDarkTheme()` return `false` (light theme) when there is no detector, the same value the existing `catch (e: Exception)` returns. +- In `Theme.desktop.kt`, `detector` is initialised from `systemThemeDetector`; `getDetector()` returns one cached instance, so both files already shared it. The change listener and the gsettings shutdown hook (#7747) are registered only when a detector exists. + +## Approaches considered + +1. Shared eager accessor with a `LinkageError` guard (chosen). +2. Lazy accessor that logs a warning: no re-entrancy, but moves the detector's start-up cost (about 190 ms measured on the baseline) from `runMigrations` to the first composition on the UI thread. +3. A guard in each file: the same fact (how to continue when JNA cannot be loaded) stated twice. +4. Guard only `Resources.desktop.kt`, the file in the report's trace: `Theme.desktop.kt` would throw the same error when the window starts. +5. Point JNA at an executable temp directory at launch: a different change (running native code extracted into a user-writable directory) and it does not help when mapping fails for another reason. + +## Scope + +Desktop only (`desktopMain`); `spec/services/theme.md` is updated to the new code. Not changed: JNA temp directory, VLC/video behaviour without JNA. A JNA native library of another version selected through an explicit `jna.boot.library.path` or `jna.nosys=false` throws `java.lang.Error`, not `LinkageError`, and is not caught; the app sets neither property.