Missing, stale, or unusable DLLs leave JNA's ordinary loading path available.
+ * Other platforms also use ordinary JNA loading. Explicit JNA loading settings
+ * and an already initialized JNA take precedence.
+ */
+final class JnaNativeLoader {
+ private static final String BOOT_PATH = "jna.boot.library.path";
+ private static final String BOOT_NAME = "jna.boot.library.name";
+
+ private JnaNativeLoader() {
+ }
+
+ /**
+ * Call before constructing a platform connector or otherwise initializing JNA.
+ */
+ static synchronized void initialize() {
+ String path;
+ String bootName;
+ try {
+ if (!Platform.isWindows() || isConfigured()) {
+ return;
+ }
+ byte[] binary = readEmbeddedLibrary();
+ if (binary == null) {
+ return;
+ }
+ Path library = bundledLibrary(binary);
+ if (library == null || isConfigured()) {
+ return;
+ }
+ path = library.getParent().toAbsolutePath().toString();
+ bootName = "jnidispatch-" + Platform.RESOURCE_PREFIX;
+ } catch (IOException | RuntimeException unavailable) {
+ // An optional optimization must not prevent JNA's ordinary loading path.
+ return;
+ }
+ loadPreparedLibrary(path, bootName);
+ }
+
+ private static byte[] readEmbeddedLibrary() throws IOException {
+ String resource = "/com/sun/jna/" + Platform.RESOURCE_PREFIX + "/jnidispatch.dll";
+ // A class literal does not initialize Native; use its loader's exact binary.
+ try (InputStream input = Native.class.getResourceAsStream(resource);
+ ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+ if (input == null) {
+ return null;
+ }
+ byte[] buffer = new byte[32768];
+ int size;
+ while ((size = input.read(buffer)) != -1) {
+ output.write(buffer, 0, size);
+ }
+ return output.toByteArray();
+ }
+ }
+
+ private static void loadPreparedLibrary(String path, String bootName) {
+ try {
+ try {
+ System.setProperty(BOOT_PATH, path);
+ System.setProperty(BOOT_NAME, bootName);
+ } catch (RuntimeException unavailable) {
+ return;
+ }
+ // JNA itself falls back to ordinary extraction if this binary cannot be loaded.
+ Class.forName(Native.class.getName(), true, Native.class.getClassLoader());
+ } catch (ClassNotFoundException impossible) {
+ throw new IllegalStateException("Cannot initialize JNA: its class loader could not find "
+ + "com.sun.jna.Native, which is required to load the native library. Ensure JNA is included "
+ + "in the bot's runtime classpath and is visible to its class loader.", impossible);
+ } finally {
+ // Do not leave a process-wide path pointing at this class loader's JNA version.
+ if (path.equals(System.getProperty(BOOT_PATH))) {
+ System.clearProperty(BOOT_PATH);
+ }
+ if (bootName.equals(System.getProperty(BOOT_NAME))) {
+ System.clearProperty(BOOT_NAME);
+ }
+ }
+ }
+
+ private static boolean isConfigured() {
+ if (Boolean.getBoolean("jna.loaded")) {
+ return true;
+ }
+ for (String setting : new String[] {BOOT_PATH, BOOT_NAME,
+ "jna.noclasspath", "jna.nosys", "jna.prefix", "jnidispatch.preserve"}) {
+ if (System.getProperty(setting) != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static Path bundledLibrary(byte[] binary) {
+ try {
+ // Resolve beside the JAR (or wrapped EXE), never relative to a launcher's working directory.
+ Path artifact = Paths.get(JnaNativeLoader.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+ Path library = artifact.resolveSibling("jnidispatch-" + Platform.RESOURCE_PREFIX + ".dll");
+ // A replaced JAR must not accidentally load a DLL left over from another JNA version.
+ return matches(library, binary) ? library : null;
+ } catch (IOException | URISyntaxException | RuntimeException unavailable) {
+ return null;
+ }
+ }
+
+ private static boolean matches(Path file, byte[] expected) throws IOException {
+ if (!Files.exists(file, NOFOLLOW_LINKS)) {
+ return false;
+ }
+ try (SeekableByteChannel channel = Files.newByteChannel(file, READ, NOFOLLOW_LINKS)) {
+ if (channel.size() != expected.length) {
+ return false;
+ }
+ ByteBuffer bytes = ByteBuffer.allocate(expected.length);
+ while (bytes.hasRemaining()) {
+ if (channel.read(bytes) < 0) {
+ return false;
+ }
+ }
+ return Arrays.equals(expected, bytes.array());
+ }
+ }
+}
diff --git a/src/test/java/bwapi/BWClientStartupTest.java b/src/test/java/bwapi/BWClientStartupTest.java
new file mode 100644
index 0000000..dd25642
--- /dev/null
+++ b/src/test/java/bwapi/BWClientStartupTest.java
@@ -0,0 +1,49 @@
+package bwapi;
+
+import org.junit.Test;
+
+import java.lang.management.BufferPoolMXBean;
+import java.lang.management.ManagementFactory;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Guards the order of startup work: attempt the BWAPI connection before creating
+ * performance metrics or allocating the large buffers used for asynchronous frames.
+ * Measures direct-buffer allocation at a mocked connection boundary, so the test
+ * needs neither a running server nor a machine-dependent elapsed-time threshold.
+ */
+public class BWClientStartupTest {
+ // Stops startGame at the mocked connection boundary, before a game loop can run.
+ private static class StopAfterConnectionAttempt extends RuntimeException {
+ }
+
+ @Test
+ public void attemptsConnectionBeforeAllocatingFrameBuffersOrMetrics() {
+ BWClient bwClient = new BWClient(new DefaultBWListener());
+ Client client = mock(Client.class);
+ bwClient.setClient(client);
+ BWClientConfiguration configuration = new BWClientConfiguration.Builder()
+ .withAsync(true)
+ .withAsyncFrameBufferCapacity(3)
+ .build();
+ BufferPoolMXBean directBuffers = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class)
+ .stream()
+ .filter(pool -> "direct".equals(pool.getName()))
+ .findFirst()
+ .get();
+ long bytesBeforeStart = directBuffers.getMemoryUsed();
+ doAnswer(invocation -> {
+ assertTrue("Connection attempts must precede allocation of even one game-state buffer",
+ directBuffers.getMemoryUsed() - bytesBeforeStart < ClientData.GameData.SIZE);
+ assertNull(bwClient.getPerformanceMetrics());
+ throw new StopAfterConnectionAttempt();
+ }).when(client).reconnect();
+
+ assertThrows(StopAfterConnectionAttempt.class, () -> bwClient.startGame(configuration));
+ }
+}
diff --git a/src/test/java/bwapi/JnaNativeLoaderTest.java b/src/test/java/bwapi/JnaNativeLoaderTest.java
new file mode 100644
index 0000000..6d53f9e
--- /dev/null
+++ b/src/test/java/bwapi/JnaNativeLoaderTest.java
@@ -0,0 +1,215 @@
+package bwapi;
+
+import com.sun.jna.Native;
+import com.sun.jna.Platform;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.assumeTrue;
+
+/**
+ * Verifies bundled Windows DLL selection and ordinary JNA fallback without a BWAPI server.
+ * Each scenario loads the real native library in a fresh JVM because native initialization
+ * cannot be reset within one JVM. Windows-specific cases are skipped on other platforms.
+ */
+public class JnaNativeLoaderTest {
+ @Rule
+ public TemporaryFolder temporary = new TemporaryFolder();
+
+ @Test
+ public void jarOnlyDeploymentUsesOrdinaryJnaLoading() throws Exception {
+ Path base = temporary.newFolder().toPath();
+ String result = finish(start(base));
+ assertExtractedLibrary(result, base);
+ assertEquals("unset", value(result, "boot"));
+ assertEquals("unset", value(result, "bootName"));
+ }
+
+ @Test
+ public void respectsExplicitLoadingSettingsAndAlreadyLoadedJna() throws Exception {
+ Path distribution = temporary.newFolder().toPath();
+ String classpath = bundledClasspath(distribution);
+ Path base = temporary.newFolder().toPath();
+ String explicit = base.resolve("explicit").toString();
+ String result = finish(startOnClasspath(base, classpath, "-Djna.boot.library.path=" + explicit));
+ assertEquals(explicit, value(result, "boot"));
+ assertExtractedLibrary(result, base);
+ result = finish(startOnClasspath(base, classpath, "-Djna.boot.library.name=custom-name"));
+ assertEquals("custom-name", value(result, "bootName"));
+ assertExtractedLibrary(result, base);
+ result = finish(startOnClasspath(base, classpath, "-Dprobe.preload=true"));
+ assertExtractedLibrary(result, base);
+ }
+
+ @Test
+ public void honorsNoUnpackWhenNoBundledLibraryIsAvailable() throws Exception {
+ Path base = temporary.newFolder().toPath();
+ Process child = start(base, "-Djna.nounpack=true");
+ String output = output(child);
+ assertNotEquals(output, 0, child.exitValue());
+ assertTrue(output, output.contains("UnsatisfiedLinkError"));
+ }
+
+ @Test
+ public void bundledWindowsLibraryIsSelectedWithoutFlagsFromAnotherWorkingDirectory() throws Exception {
+ assumeTrue(Platform.isWindows());
+ Path distribution = temporary.newFolder("bot with spaces #").toPath();
+ String classpath = bundledClasspath(distribution);
+ Path base = temporary.newFolder("unrelated working directory").toPath();
+ String result = finish(startOnClasspath(base, classpath));
+ Path expected = distribution.resolve("jnidispatch-" + Platform.RESOURCE_PREFIX + ".dll");
+ assertTrue(result, Files.isSameFile(expected, Paths.get(value(result, "native"))));
+ assertEquals("unset", value(result, "boot"));
+ assertEquals("unset", value(result, "bootName"));
+ // Disabling extraction still permits loading the existing bundled DLL.
+ result = finish(startOnClasspath(base, classpath, "-Djna.nounpack=true"));
+ assertTrue(result, Files.isSameFile(expected, Paths.get(value(result, "native"))));
+ }
+
+ @Test
+ public void staleOrMissingBundledWindowsLibraryUsesOrdinaryJnaLoading() throws Exception {
+ assumeTrue(Platform.isWindows());
+ Path distribution = temporary.newFolder().toPath();
+ String classpath = bundledClasspath(distribution);
+ Path base = temporary.newFolder().toPath();
+ Path library = distribution.resolve("jnidispatch-" + Platform.RESOURCE_PREFIX + ".dll");
+ byte[] stale = Files.readAllBytes(library);
+ stale[stale.length - 1] ^= 1;
+ Files.write(library, stale);
+ String result = finish(startOnClasspath(base, classpath));
+ assertExtractedLibrary(result, base);
+ assertArrayEquals(stale, Files.readAllBytes(library));
+ Files.delete(library);
+ assertExtractedLibrary(finish(startOnClasspath(base, classpath)), base);
+ }
+
+ @Test
+ public void bundledWindowsLibrariesAreIgnoredOnOtherPlatforms() throws Exception {
+ assumeTrue(!Platform.isWindows());
+ Path distribution = temporary.newFolder().toPath();
+ String classpath = bundledClasspath(distribution);
+ Path base = temporary.newFolder().toPath();
+ assertExtractedLibrary(finish(startOnClasspath(base, classpath)), base);
+ }
+
+ private void assertExtractedLibrary(String output, Path directory) throws Exception {
+ Path library = Paths.get(value(output, "native"));
+ assertTrue(output, Files.isSameFile(directory, library.getParent()));
+ }
+
+ /**
+ * Builds a minimal distribution fixture. Loading the helper from bot.jar gives
+ * it a real artifact location for finding adjacent DLLs; the rest of the classes
+ * remain on the test classpath. All Windows architectures are included together.
+ */
+ private String bundledClasspath(Path distribution) throws Exception {
+ Path jar = distribution.resolve("bot.jar");
+ try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(jar));
+ InputStream input = JnaNativeLoader.class.getResourceAsStream("JnaNativeLoader.class")) {
+ output.putNextEntry(new JarEntry("bwapi/JnaNativeLoader.class"));
+ byte[] bytes = new byte[4096];
+ int size;
+ while ((size = input.read(bytes)) != -1) {
+ output.write(bytes, 0, size);
+ }
+ output.closeEntry();
+ }
+ for (String arch : new String[] {"win32-x86", "win32-x86-64", "win32-aarch64"}) {
+ String resource = "com/sun/jna/" + arch + "/jnidispatch.dll";
+ Path library = distribution.resolve("jnidispatch-" + arch + ".dll");
+ Files.createDirectories(library.getParent());
+ try (InputStream input = Native.class.getResourceAsStream("/" + resource)) {
+ Files.copy(input, library);
+ }
+ }
+ return jar + System.getProperty("path.separator") + testClasspath();
+ }
+
+ private String testClasspath() {
+ return System.getProperty("surefire.test.class.path", System.getProperty("java.class.path"));
+ }
+
+ private Process start(Path base, String... options) throws Exception {
+ return startOnClasspath(base, testClasspath(), options);
+ }
+
+ // Use the test JVM's runtime and an isolated working/temp directory for each scenario.
+ private Process startOnClasspath(Path base, String classpath, String... options) throws Exception {
+ String executable = System.getProperty("os.name").startsWith("Windows") ? "java.exe" : "java";
+ List