Summary
When two or more Compose Multiplatform projects that both depend on composenativetray are running at the same time on Windows (common in multi-project development), the second project to start throws an AccessDeniedException while trying to extract/replace the native library, crashing the entire tray subsystem.
Environment
- OS: Windows 11 Pro (10.0.26200)
- composenativetray version: 2.0.3
- nucleus.core-runtime version: 2.3.2
- JVM: Java 25
Steps to Reproduce
- Have two separate Compose Multiplatform desktop projects, both depending on
composenativetray.
- Launch the first project — tray initializes successfully.
- Launch the second project while the first is still running.
- The second project crashes at
WindowsTrayManager initialization.
Stack Trace
java.lang.ExceptionInInitializerError
at dev.nucleusframework.composenativetray.lib.windows.WindowsTrayManager.initialize$lambda$0$0(WindowsTrayManager.kt:92)
at java.base/java.lang.Thread.run(Thread.java:1474)
Caused by: java.nio.file.AccessDeniedException: C:\Users\<user>\AppData\Local\composetray\native\win32-x86-64\WinTray.dll
at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:91)
at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:106)
at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:112)
at java.base/sun.nio.fs.WindowsFileCopy.move(WindowsFileCopy.java:388)
at java.base/sun.nio.fs.WindowsFileSystemProvider.move(WindowsFileSystemProvider.java:287)
at java.base/java.nio.file.Files.move(Files.java:1319)
at dev.nucleusframework.composenativetray.utils.NativeLibraryLoader.extractToCache(NativeLibraryLoader.kt:78)
at dev.nucleusframework.composenativetray.utils.NativeLibraryLoader.load(NativeLibraryLoader.kt:40)
at dev.nucleusframework.composenativetray.lib.windows.WindowsNativeBridge.<clinit>(WindowsNativeBridge.kt:12)
Root Cause
NativeLibraryLoader.extractToCache() always extracts the bundled WinTray.dll to a fixed shared path (AppData\Local\composetray\native\win32-x86-64\WinTray.dll) and replaces it via Files.move().
On Windows, a DLL that is already loaded by a running JVM process is locked by the OS. A second process attempting Files.move() to overwrite the locked file receives AccessDeniedException, causing ExceptionInInitializerError and taking down the entire tray system.
Expected Behavior
If the DLL already exists at the cache path and its content matches the bundled version (e.g. same checksum), the extraction step should be skipped entirely rather than attempting an unnecessary overwrite. This would allow multiple projects to coexist on the same machine without conflict.
Suggested Fix
In NativeLibraryLoader.extractToCache(), before calling Files.move(), check whether the target file already exists and its content is identical to the bundled resource (e.g. compare file size or SHA-256). If they match, skip the move and proceed directly to System.load().
// Skip extraction if the cached DLL is already up-to-date
if (targetFile.exists() && targetFile.isSameContent(bundledResource)) {
System.load(targetFile.absolutePath)
return
}
// Otherwise proceed with extraction + move
This is consistent with how most JNA/JNI native-library loaders handle the "already cached" case (e.g. jna, lwjgl).
Summary
When two or more Compose Multiplatform projects that both depend on
composenativetrayare running at the same time on Windows (common in multi-project development), the second project to start throws anAccessDeniedExceptionwhile trying to extract/replace the native library, crashing the entire tray subsystem.Environment
Steps to Reproduce
composenativetray.WindowsTrayManagerinitialization.Stack Trace
Root Cause
NativeLibraryLoader.extractToCache()always extracts the bundledWinTray.dllto a fixed shared path (AppData\Local\composetray\native\win32-x86-64\WinTray.dll) and replaces it viaFiles.move().On Windows, a DLL that is already loaded by a running JVM process is locked by the OS. A second process attempting
Files.move()to overwrite the locked file receivesAccessDeniedException, causingExceptionInInitializerErrorand taking down the entire tray system.Expected Behavior
If the DLL already exists at the cache path and its content matches the bundled version (e.g. same checksum), the extraction step should be skipped entirely rather than attempting an unnecessary overwrite. This would allow multiple projects to coexist on the same machine without conflict.
Suggested Fix
In
NativeLibraryLoader.extractToCache(), before callingFiles.move(), check whether the target file already exists and its content is identical to the bundled resource (e.g. compare file size or SHA-256). If they match, skip the move and proceed directly toSystem.load().This is consistent with how most JNA/JNI native-library loaders handle the "already cached" case (e.g.
jna,lwjgl).