diff --git a/lib/src/main/java/org/automerge/LoadLibrary.java b/lib/src/main/java/org/automerge/LoadLibrary.java index d4c932d..b9d99d8 100644 --- a/lib/src/main/java/org/automerge/LoadLibrary.java +++ b/lib/src/main/java/org/automerge/LoadLibrary.java @@ -6,13 +6,13 @@ import java.nio.file.*; import java.util.Locale; import java.util.Optional; -import java.util.Properties; import java.util.UUID; import java.util.stream.Stream; -class LoadLibrary { +public class LoadLibrary { private static class Library { + public String target; public String prefix; public String suffix; @@ -124,16 +124,44 @@ public boolean isAndroid() { // The extension added to the library name to create a lockfile for it private static final String LIBRARY_LOCK_EXT = ".lck"; - static boolean loaded = false; + static volatile boolean loaded = false; + static volatile boolean versionChecked = false; - public static synchronized void initialize() { - // Only do this on first load and _dont_ do it on android, where libraries - // are provided by jniLibs and the version.properties file is removed from the - // jar by the build process - if (!loaded && !CURRENT_PLATFORM.isAndroid()) { - cleanup(); + public static void initialize() { + // Early return if already loaded and version checked + if (loaded && versionChecked) { + return; + } + + synchronized (LoadLibrary.class) { + // Double-check after acquiring lock + if (loaded && versionChecked) { + return; + } + + // Load the library if not already loaded + if (!loaded) { + // Only do this on first load and _dont_ do it on android, where libraries + // are provided by jniLibs + if (!CURRENT_PLATFORM.isAndroid()) { + cleanup(); + } + loadAutomergeJniLib(); + loaded = true; + } + } + + // Perform version check OUTSIDE the synchronized block to avoid deadlock + // when AutomergeSys static initializer calls back to initialize() + if (!versionChecked) { + String expectedLibVersion = BuildInfo.getExpectedRustLibVersion(); + String actualLibVersion = AutomergeSys.rustLibVersion(); + if (!expectedLibVersion.equals(actualLibVersion)) { + throw new RuntimeException("Automerge native library version mismatch. Expected " + expectedLibVersion + + " but got " + actualLibVersion); + } + versionChecked = true; } - loadAutomergeJniLib(); } private static File getTempDir() { @@ -152,7 +180,7 @@ private static File getTempDir() { * instances of the JVM which have loaded the library. */ static void cleanup() { - String searchPattern = "automerge-jni-" + getVersion(); + String searchPattern = "automerge-jni-" + BuildInfo.getExpectedRustLibVersion(); try (Stream dirList = Files.list(getTempDir().toPath())) { dirList.filter(path -> !path.getFileName().toString().endsWith(LIBRARY_LOCK_EXT) @@ -173,7 +201,7 @@ static void cleanup() { private static void extractAndLoadLibraryFile(Library library, String targetFolder) { String uuid = UUID.randomUUID().toString(); - String extractedLibFileName = String.format("automerge-jni-%s-%s-%s.%s", getVersion(), uuid, library.target, + String extractedLibFileName = String.format("automerge-jni-%s-%s-%s.%s", BuildInfo.getExpectedRustLibVersion(), uuid, library.target, library.suffix); String extractedLckFileName = extractedLibFileName + LIBRARY_LOCK_EXT; @@ -230,17 +258,16 @@ private static InputStream getResourceAsStream(String name) { *

* We first try the system library path, then if that fails we try and load one * of the bundled libraries from this jar. + * + *

+ * Note: This method does NOT set the loaded flag. That is the responsibility + * of the caller (initialize() method) to ensure proper synchronization. */ private static void loadAutomergeJniLib() { - if (loaded) { - return; - } - String libName = "automerge_jni_" + BuildInfo.getExpectedRustLibVersion().replace('.', '_'); // Try System.loadLibrary first try { System.loadLibrary(libName); - loaded = true; return; } catch (UnsatisfiedLinkError e) { if (CURRENT_PLATFORM.isAndroid()) { @@ -259,31 +286,5 @@ private static void loadAutomergeJniLib() { Library lib = CURRENT_PLATFORM.library().get(); // Try extracting the library from jar extractAndLoadLibraryFile(lib, tempFolder); - - // Check that we have the correct version of the library (BuildInfo is - // generated by gradle and contains the version of the rust library we - // were built against) - String expectedLibVersion = BuildInfo.getExpectedRustLibVersion(); - String actualLibVersion = AutomergeSys.rustLibVersion(); - if (!expectedLibVersion.equals(actualLibVersion)) { - throw new RuntimeException("Automerge native library version mismatch. Expected " + expectedLibVersion - + " but got " + actualLibVersion); - } - - loaded = true; - } - - public static String getVersion() { - InputStream input = getResourceAsStream("version.properties"); - - String version = "unknown"; - try { - Properties versionData = new Properties(); - versionData.load(input); - version = versionData.getProperty("version", version); - return version; - } catch (IOException e) { - throw new RuntimeException("unable to load version properties", e); - } } }