diff --git a/affinity/pom.xml b/affinity/pom.xml
index 45768cd56..250c5c73d 100644
--- a/affinity/pom.xml
+++ b/affinity/pom.xml
@@ -77,6 +77,12 @@
junit-jupiter
test
+
+ net.java.dev.jna
+ jna-jpms
+ 5.17.0
+ test
+
org.slf4j
diff --git a/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity.cpp b/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity.cpp
index f13d566a1..af265dabe 100644
--- a/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity.cpp
+++ b/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity.cpp
@@ -12,22 +12,38 @@
#include
#include
#include
+ #include
+ #include
#endif
-#include
#include "software_chronicle_enterprise_internals_impl_NativeAffinity.h"
+#ifndef __linux__
+static void throwUnsupportedOperation(JNIEnv *env, const char *message) {
+ jclass exClass = env->FindClass("java/lang/UnsupportedOperationException");
+ if (exClass != NULL) {
+ env->ThrowNew(exClass, message);
+ }
+}
+#endif
+
+#ifdef __linux__
+static void throwRuntimeException(JNIEnv *env, const char *message) {
+ jclass exClass = env->FindClass("java/lang/RuntimeException");
+ if (exClass != NULL) {
+ env->ThrowNew(exClass, message);
+ }
+}
+#endif
+
/*
* Class: software_chronicle_enterprise_internals_impl_NativeAffinity
* Method: getAffinity0
- * Signature: ()J
+ * Signature: ()[B
*/
JNIEXPORT jbyteArray JNICALL Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getAffinity0
- (JNIEnv *env, jclass c)
+ (JNIEnv *env, jclass c)
{
#ifdef __linux__
- // The default size of the structure supports 1024 CPUs, should be enough
- // for now In the future we can use dynamic sets, which can support more
- // CPUs, given OS can handle them as well
cpu_set_t mask;
const size_t size = sizeof(mask);
@@ -37,21 +53,23 @@ JNIEXPORT jbyteArray JNICALL Java_software_chronicle_enterprise_internals_impl_N
return NULL;
}
- jbyteArray ret = env->NewByteArray(size);
- jbyte* bytes = env->GetByteArrayElements(ret, 0);
- memcpy(bytes, &mask, size);
- env->SetByteArrayRegion(ret, 0, size, bytes);
+ jbyteArray ret = env->NewByteArray((jsize) size);
+ if (ret == NULL) {
+ return NULL;
+ }
+ env->SetByteArrayRegion(ret, 0, (jsize) size, (const jbyte *) &mask);
return ret;
#else
- throw std::runtime_error("Not supported");
+ throwUnsupportedOperation(env, "NativeAffinity.getAffinity0 is only supported on Linux");
+ return NULL;
#endif
}
/*
- * Class: software_chronicle_enterprise_internals_NativeAffinity
+ * Class: software_chronicle_enterprise_internals_impl_NativeAffinity
* Method: setAffinity0
- * Signature: (J)V
+ * Signature: ([B)V
*/
JNIEXPORT void JNICALL Java_software_chronicle_enterprise_internals_impl_NativeAffinity_setAffinity0
(JNIEnv *env, jclass c, jbyteArray affinity)
@@ -61,12 +79,26 @@ JNIEXPORT void JNICALL Java_software_chronicle_enterprise_internals_impl_NativeA
const size_t size = sizeof(mask);
CPU_ZERO(&mask);
- jbyte* bytes = env->GetByteArrayElements(affinity, 0);
- memcpy(&mask, bytes, size);
+ jsize length = env->GetArrayLength(affinity);
+ if (length > 0) {
+ jsize copyLength = length < (jsize) size ? length : (jsize) size;
+ env->GetByteArrayRegion(affinity, 0, copyLength, (jbyte *) &mask);
+ if (env->ExceptionCheck()) {
+ return;
+ }
+ }
- sched_setaffinity(0, size, &mask);
+ int res = sched_setaffinity(0, size, &mask);
+ if (res != 0) {
+ const int error = errno;
+ char message[256];
+ snprintf(message, sizeof(message),
+ "sched_setaffinity(thread=0, maskBytes=%d) failed: errno=%d (%s)",
+ (int) length, error, strerror(error));
+ throwRuntimeException(env, message);
+ }
#else
- throw std::runtime_error("Not supported");
+ throwUnsupportedOperation(env, "NativeAffinity.setAffinity0 is only supported on Linux");
#endif
}
@@ -77,11 +109,11 @@ JNIEXPORT void JNICALL Java_software_chronicle_enterprise_internals_impl_NativeA
*/
JNIEXPORT jint JNICALL Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getProcessId0
(JNIEnv *env, jclass c) {
-#ifndef __linux__
- throw std::runtime_error("Not supported");
+#ifdef __linux__
+ return (jint) getpid();
#else
-
- return (jint) getpid();
+ throwUnsupportedOperation(env, "NativeAffinity.getProcessId0 is only supported on Linux");
+ return (jint) -1;
#endif
}
@@ -92,11 +124,11 @@ JNIEXPORT jint JNICALL Java_software_chronicle_enterprise_internals_impl_NativeA
*/
JNIEXPORT jint JNICALL Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getThreadId0
(JNIEnv *env, jclass c) {
-#ifndef __linux__
- throw std::runtime_error("Not supported");
-#else
-
+#ifdef __linux__
return (jint) (pid_t) syscall (SYS_gettid);
+#else
+ throwUnsupportedOperation(env, "NativeAffinity.getThreadId0 is only supported on Linux");
+ return (jint) -1;
#endif
}
@@ -107,11 +139,10 @@ JNIEXPORT jint JNICALL Java_software_chronicle_enterprise_internals_impl_NativeA
*/
JNIEXPORT jint JNICALL Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getCpu0
(JNIEnv *env, jclass c) {
-#ifndef __linux__
- throw std::runtime_error("Not supported");
+#ifdef __linux__
+ return (jint) sched_getcpu();
#else
-
- return (jint) sched_getcpu();
+ throwUnsupportedOperation(env, "NativeAffinity.getCpu0 is only supported on Linux");
+ return (jint) -1;
#endif
}
-
diff --git a/affinity/src/main/java/net/openhft/affinity/Affinity.java b/affinity/src/main/java/net/openhft/affinity/Affinity.java
index 8dadd8a15..feb353569 100644
--- a/affinity/src/main/java/net/openhft/affinity/Affinity.java
+++ b/affinity/src/main/java/net/openhft/affinity/Affinity.java
@@ -3,7 +3,6 @@
*/
package net.openhft.affinity;
-import com.sun.jna.Native;
import net.openhft.affinity.impl.*;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
@@ -11,6 +10,7 @@
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.util.BitSet;
@@ -25,43 +25,47 @@ public enum Affinity {
static final Logger LOGGER = LoggerFactory.getLogger(Affinity.class);
@NotNull
private static final IAffinity AFFINITY_IMPL;
- private static Boolean JNAAvailable;
+ private static volatile Boolean jnaAvailable;
static {
- String osName = System.getProperty("os.name");
- if (osName.contains("Win") && isWindowsJNAAffinityUsable()) {
- LOGGER.trace("Using Windows JNA-based affinity control implementation");
- AFFINITY_IMPL = WindowsJNAAffinity.INSTANCE;
-
- } else if (osName.contains("x")) {
- /*if (osName.startsWith("Linux") && NativeAffinity.LOADED) {
- LOGGER.trace("Using Linux JNI-based affinity control implementation");
- AFFINITY_IMPL = NativeAffinity.INSTANCE;
- } else*/
- if (osName.startsWith("Linux") && isLinuxJNAAffinityUsable()) {
- LOGGER.trace("Using Linux JNA-based affinity control implementation");
- AFFINITY_IMPL = LinuxJNAAffinity.INSTANCE;
-
- } else if (isPosixJNAAffinityUsable()) {
- LOGGER.trace("Using Posix JNA-based affinity control implementation");
- AFFINITY_IMPL = PosixJNAAffinity.INSTANCE;
+ IAffinity impl;
+ try {
+ String osName = System.getProperty("os.name");
+ if (osName.contains("Win") && isWindowsJNAAffinityUsable()) {
+ LOGGER.trace("Using Windows JNA-based affinity control implementation");
+ impl = WindowsJNAAffinity.INSTANCE;
+
+ } else if (osName.contains("x")) {
+ if (osName.startsWith("Linux") && isLinuxJNAAffinityUsable()) {
+ LOGGER.trace("Using Linux JNA-based affinity control implementation");
+ impl = LinuxJNAAffinity.INSTANCE;
+
+ } else if (isPosixJNAAffinityUsable()) {
+ LOGGER.trace("Using Posix JNA-based affinity control implementation");
+ impl = PosixJNAAffinity.INSTANCE;
+
+ } else {
+ LOGGER.info("Unsupported POSIX OS: {} with an 'x'. Using dummy affinity control implementation", osName);
+ impl = NullAffinity.INSTANCE;
+ }
+ } else if (osName.contains("Mac") && isMacJNAAffinityUsable()) {
+ LOGGER.trace("Using MAC OSX JNA-based thread id implementation");
+ impl = OSXJNAAffinity.INSTANCE;
+
+ } else if (osName.contains("SunOS") && isSolarisJNAAffinityUsable()) {
+ LOGGER.trace("Using Solaris JNA-based thread id implementation");
+ impl = SolarisJNAAffinity.INSTANCE;
} else {
- LOGGER.info("Using dummy affinity control implementation");
- AFFINITY_IMPL = NullAffinity.INSTANCE;
+ LOGGER.info("Unsupported OS: {}. Using dummy affinity control implementation", osName);
+ impl = NullAffinity.INSTANCE;
}
- } else if (osName.contains("Mac") && isMacJNAAffinityUsable()) {
- LOGGER.trace("Using MAC OSX JNA-based thread id implementation");
- AFFINITY_IMPL = OSXJNAAffinity.INSTANCE;
-
- } else if (osName.contains("SunOS") && isSolarisJNAAffinityUsable()) {
- LOGGER.trace("Using Solaris JNA-based thread id implementation");
- AFFINITY_IMPL = SolarisJNAAffinity.INSTANCE;
-
- } else {
- LOGGER.info("Using dummy affinity control implementation");
- AFFINITY_IMPL = NullAffinity.INSTANCE;
+ } catch (LinkageError | RuntimeException t) {
+ // Optional native initialisation may fail; VM errors and assertion failures must propagate.
+ LOGGER.warn("Falling back to dummy affinity control implementation because native init failed", t);
+ impl = NullAffinity.INSTANCE;
}
+ AFFINITY_IMPL = impl;
}
public static IAffinity getAffinityImpl() {
@@ -72,7 +76,7 @@ private static boolean isWindowsJNAAffinityUsable() {
if (isJNAAvailable()) {
try {
return WindowsJNAAffinity.LOADED;
- } catch (Throwable t) {
+ } catch (LinkageError | RuntimeException t) {
logThrowable(t, "Windows JNA-based affinity not usable because it failed to load!");
return false;
}
@@ -86,7 +90,7 @@ private static boolean isPosixJNAAffinityUsable() {
if (isJNAAvailable()) {
try {
return PosixJNAAffinity.LOADED;
- } catch (Throwable t) {
+ } catch (LinkageError | RuntimeException t) {
logThrowable(t, "Posix JNA-based affinity not usable because it failed to load!");
return false;
}
@@ -100,7 +104,7 @@ private static boolean isLinuxJNAAffinityUsable() {
if (isJNAAvailable()) {
try {
return LinuxJNAAffinity.LOADED;
- } catch (Throwable t) {
+ } catch (LinkageError | RuntimeException t) {
logThrowable(t, "Linux JNA-based affinity not usable because it failed to load!");
return false;
}
@@ -173,22 +177,44 @@ public static void setThreadId() {
}
}
+ @SuppressWarnings("removal") // ThreadDeath must propagate on supported older JDKs.
public static boolean isJNAAvailable() {
- if (JNAAvailable == null) {
- int majorVersion = Integer.parseInt(Native.VERSION.split("\\.")[0]);
- if (majorVersion < 5) {
- LOGGER.warn("Affinity library requires JNA version >= 5");
- JNAAvailable = false;
- } else {
- try {
- Class.forName("com.sun.jna.Platform");
- JNAAvailable = true;
- } catch (ClassNotFoundException ignored) {
- JNAAvailable = false;
+ Boolean available = jnaAvailable;
+ if (available == null) {
+ synchronized (Affinity.class) {
+ available = jnaAvailable;
+ if (available == null) {
+ boolean result;
+ try {
+ Class> nativeClass = Class.forName("com.sun.jna.Native");
+ // Access the inherited public field through Native without opening JNA's module.
+ String version = (String) MethodHandles.publicLookup()
+ .findStaticGetter(nativeClass, "VERSION", String.class).invokeExact();
+ int majorVersion = version == null ? 0 : Integer.parseInt(version.split("\\.")[0]);
+ if (majorVersion < 5) {
+ LOGGER.warn("Affinity library requires JNA version >= 5");
+ result = false;
+ } else {
+ try {
+ Class.forName("com.sun.jna.Platform");
+ result = true;
+ } catch (ClassNotFoundException ignored) {
+ result = false;
+ }
+ }
+ } catch (VirtualMachineError | ThreadDeath | AssertionError fatal) {
+ throw fatal;
+ } catch (Throwable t) {
+ // JNA also reports an incompatible jnidispatch with a plain Error.
+ LOGGER.warn("JNA not available, falling back to NullAffinity", t);
+ result = false;
+ }
+ available = result;
+ jnaAvailable = available;
}
}
}
- return JNAAvailable;
+ return available;
}
public static AffinityLock acquireLock() {
diff --git a/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java b/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java
index a52404bba..46f00d4cb 100644
--- a/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java
+++ b/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java
@@ -33,6 +33,11 @@ public class LinuxHelper {
version = ver;
}
+ /**
+ * Returns the CPU affinity mask of the calling thread.
+ *
+ * @return the processors on which the calling thread is permitted to run
+ */
public static
@NotNull
cpu_set_t sched_getaffinity() {
@@ -52,10 +57,16 @@ cpu_set_t sched_getaffinity() {
return cpuset;
}
+ /**
+ * Sets the CPU affinity mask of the calling thread.
+ */
public static void sched_setaffinity(final BitSet affinity) {
sched_setaffinity(0, affinity);
}
+ /**
+ * Sets affinity for the thread identified by {@code pid}; zero selects the calling thread.
+ */
public static void sched_setaffinity(final int pid, final BitSet affinity) {
final CLibrary lib = CLibrary.INSTANCE;
final cpu_set_t cpuset = new cpu_set_t();
diff --git a/affinity/src/main/java/net/openhft/affinity/impl/LinuxJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/LinuxJNAAffinity.java
index d16fc3beb..eaae34d57 100644
--- a/affinity/src/main/java/net/openhft/affinity/impl/LinuxJNAAffinity.java
+++ b/affinity/src/main/java/net/openhft/affinity/impl/LinuxJNAAffinity.java
@@ -45,6 +45,9 @@ public enum LinuxJNAAffinity implements IAffinity {
private final ThreadLocal THREAD_ID = new ThreadLocal<>();
+ /**
+ * Returns the CPU affinity mask of the calling thread.
+ */
@Override
public BitSet getAffinity() {
final LinuxHelper.cpu_set_t cpuset = LinuxHelper.sched_getaffinity();
@@ -58,6 +61,9 @@ public BitSet getAffinity() {
return ret;
}
+ /**
+ * Sets the CPU affinity mask of the calling thread.
+ */
@Override
public void setAffinity(final BitSet affinity) {
LinuxHelper.sched_setaffinity(affinity);
diff --git a/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java
index 39aed93ab..55cad7a39 100644
--- a/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java
+++ b/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java
@@ -28,7 +28,6 @@ public enum PosixJNAAffinity implements IAffinity {
INSTANCE;
public static final boolean LOADED;
private static final Logger LOGGER = LoggerFactory.getLogger(PosixJNAAffinity.class);
- private static final String LIBRARY_NAME = Platform.isWindows() ? "msvcrt" : "c";
private static final int PROCESS_ID;
private static final int SYS_gettid = Utilities.is64Bit() ? 186 : 224;
private static final Object[] NO_ARGS = {};
@@ -177,7 +176,7 @@ public int getThreadId() {
* @author BegemoT
*/
interface CLibrary extends Library {
- CLibrary INSTANCE = Native.load(LIBRARY_NAME, CLibrary.class);
+ CLibrary INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c", CLibrary.class);
int sched_setaffinity(final int pid,
final int cpusetsize,
diff --git a/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java b/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java
index eb2394ddd..e4c46fe7b 100644
--- a/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java
+++ b/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java
@@ -14,6 +14,7 @@
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.NoSuchFileException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.OpenOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
@@ -158,7 +159,7 @@ private LockReference tryAcquireLockOnFile(int id, String metaInfo) throws IOExc
}
private void writeMetaInfoToFile(FileChannel fc, String metaInfo) throws IOException {
- byte[] content = String.format("%s%n%s", metaInfo, dfTL.get().format(new Date())).getBytes();
+ byte[] content = String.format("%s%n%s", metaInfo, dfTL.get().format(new Date())).getBytes(StandardCharsets.UTF_8);
ByteBuffer buffer = ByteBuffer.wrap(content);
while (buffer.hasRemaining()) {
//noinspection ResultOfMethodCallIgnored
@@ -211,7 +212,7 @@ public String getMetaInfo(int id) throws IOException {
private String readMetaInfoFromLockFileChannel(File lockFile, FileChannel lockFileChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(64);
int len = lockFileChannel.read(buffer, 0);
- String content = len < 1 ? "" : new String(buffer.array(), 0, len);
+ String content = len < 1 ? "" : new String(buffer.array(), 0, len, StandardCharsets.UTF_8);
if (content.isEmpty()) {
LOGGER.warn("Empty lock file {}", lockFile.getAbsolutePath());
return null;
diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityInitializationTest.java b/affinity/src/test/java/net/openhft/affinity/AffinityInitializationTest.java
new file mode 100644
index 000000000..03057efa8
--- /dev/null
+++ b/affinity/src/test/java/net/openhft/affinity/AffinityInitializationTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0
+ */
+package net.openhft.affinity;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.JRE;
+import org.junit.jupiter.api.condition.OS;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@EnabledOnOs(OS.LINUX)
+public class AffinityInitializationTest extends AffinityTestProcess {
+ @Test
+ void selectsLinuxJnaInFreshJvm() throws Exception {
+ runProbe(AffinityInitializationTest.class, "normal", true);
+ }
+
+ @Test
+ void fallsBackWhenJnaIsAbsentFromClasspath() throws Exception {
+ runProbe(AffinityInitializationTest.class, "absent", false);
+ }
+
+ @Test
+ void fallsBackWhenJnaNativeLoadingFails() throws Exception {
+ // JNA classes remain present, but neither native search path may provide jnidispatch.
+ runProbe(AffinityInitializationTest.class, "native-failure", true,
+ "-Djna.boot.library.path=" + directory.toAbsolutePath(),
+ "-Djna.nosys=true", "-Djna.noclasspath=true");
+ }
+
+ @Test
+ void fallsBackWhenJnaNativeVersionIsIncompatible() throws Exception {
+ // The newer fixture's native ABI is incompatible with the BOM's JNA 5.5 Java classes.
+ try (JarFile jar = new JarFile(jnaModuleJar())) {
+ String resource = "com/sun/jna/" + com.sun.jna.Platform.RESOURCE_PREFIX + "/libjnidispatch.so";
+ JarEntry entry = jar.getJarEntry(resource);
+ assertNotNull(entry, resource);
+ try (InputStream input = jar.getInputStream(entry)) {
+ Files.copy(input, directory.resolve("libjnidispatch.so"));
+ }
+ }
+ String log = runProbe(AffinityInitializationTest.class, "native-failure", true,
+ "-Djna.boot.library.path=" + directory.toAbsolutePath(),
+ "-Djna.nosys=true", "-Djna.noclasspath=true");
+ assertTrue(log.contains("java.lang.Error:"), log);
+ assertTrue(log.contains("incompatible JNA native library"), log);
+ }
+
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_9)
+ void selectsJnaFromExportedButUnopenedModule() throws Exception {
+ runProbe(AffinityInitializationTest.class, "module", false,
+ "--module-path=" + jnaModuleJar(), "--add-modules=com.sun.jna");
+ }
+
+ private static String jnaModuleJar() {
+ return Arrays.stream(testClasspath().split(File.pathSeparator))
+ .filter(entry -> new File(entry).getName().startsWith("jna-jpms-"))
+ .findFirst().orElseThrow(() -> new AssertionError("Missing JNA module test fixture"));
+ }
+
+ @Test
+ @SuppressWarnings("removal") // ThreadDeath remains relevant to supported older JDKs.
+ void propagatesVmThreadTerminationAndAssertionFailures() throws Exception {
+ String[] entries = testClasspath().split(File.pathSeparator);
+ URL[] urls = new URL[entries.length];
+ for (int i = 0; i < entries.length; i++) {
+ urls[i] = new File(entries[i]).toURI().toURL();
+ }
+ for (Error failure : new Error[]{new OutOfMemoryError("test loading failure"), new ThreadDeath(),
+ new AssertionError("test loading failure")}) {
+ try (URLClassLoader loader = new URLClassLoader(urls, null) {
+ @Override
+ protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ if (name.equals("com.sun.jna.Native")) {
+ throw failure;
+ }
+ return super.loadClass(name, resolve);
+ }
+ }) {
+ Error thrown = assertThrows(Error.class,
+ () -> Class.forName("net.openhft.affinity.Affinity", true, loader));
+ assertSame(failure, thrown);
+ }
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ String scenario = System.getProperty("affinity.test.scenario");
+ ClassLoader loader = AffinityInitializationTest.class.getClassLoader();
+ if (scenario.equals("absent")) {
+ assertThrows(ClassNotFoundException.class, () -> Class.forName("com.sun.jna.Native", false, loader));
+ assertThrows(ClassNotFoundException.class, () -> Class.forName("com.sun.jna.Platform", false, loader));
+ } else {
+ assertNotNull(Class.forName("com.sun.jna.Native", false, loader));
+ }
+ if (scenario.equals("normal") || scenario.equals("module")) {
+ assertTrue(Affinity.isJNAAvailable());
+ assertEquals("net.openhft.affinity.impl.LinuxJNAAffinity", Affinity.getAffinityImpl().getClass().getName());
+ assertFalse(Affinity.getAffinity().isEmpty());
+ if (scenario.equals("module")) {
+ // Reflection here keeps these tests compilable on Java 8; the module run requires Java 9+.
+ Object module = Class.class.getMethod("getModule").invoke(Class.forName("com.sun.jna.Native"));
+ assertEquals(Boolean.TRUE, module.getClass().getMethod("isNamed").invoke(module));
+ assertEquals(Boolean.TRUE, module.getClass().getMethod("isExported", String.class).invoke(module, "com.sun.jna"));
+ assertEquals(Boolean.FALSE, module.getClass().getMethod("isOpen", String.class).invoke(module, "com.sun.jna"));
+ }
+ } else {
+ assertFalse(Affinity.isJNAAvailable());
+ assertEquals("net.openhft.affinity.impl.NullAffinity", Affinity.getAffinityImpl().getClass().getName());
+ assertTrue(Affinity.getAffinity().isEmpty());
+ if (scenario.equals("native-failure")) {
+ // The real Native class failed initialisation; this is not a mocked availability flag.
+ assertThrows(NoClassDefFoundError.class, () -> Class.forName("com.sun.jna.Native", true, loader));
+ }
+ }
+ System.out.println("PASS " + scenario);
+ }
+}
diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityTestProcess.java b/affinity/src/test/java/net/openhft/affinity/AffinityTestProcess.java
new file mode 100644
index 000000000..7d8ed3061
--- /dev/null
+++ b/affinity/src/test/java/net/openhft/affinity/AffinityTestProcess.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0
+ */
+package net.openhft.affinity;
+
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+
+abstract class AffinityTestProcess {
+ @TempDir
+ Path directory;
+
+ static String testClasspath() {
+ return System.getProperty("surefire.test.class.path", System.getProperty("java.class.path"));
+ }
+
+ String runProbe(Class> probe, String scenario, boolean withJna, String... options) throws Exception {
+ String classpath = Arrays.stream(testClasspath().split(File.pathSeparator))
+ .filter(entry -> withJna || !new File(entry).getName().startsWith("jna-"))
+ .collect(Collectors.joining(File.pathSeparator));
+ Path output = directory.resolve(scenario + ".log");
+ List command = new ArrayList<>();
+ command.add(new File(System.getProperty("java.home"), "bin/java").getAbsolutePath());
+ command.addAll(Arrays.asList(options));
+ // Avoid Java 8 launcher argument-conversion warnings before the checked JNI calls.
+ command.add("-Daffinity.test.scenario=" + scenario);
+ command.add("-cp");
+ command.add(classpath);
+ command.add(probe.getName());
+ Process process = new ProcessBuilder(command).redirectErrorStream(true).redirectOutput(output.toFile()).start();
+ boolean finished;
+ try {
+ finished = process.waitFor(30, TimeUnit.SECONDS);
+ } finally {
+ if (process.isAlive()) {
+ process.destroyForcibly();
+ process.waitFor(5, TimeUnit.SECONDS);
+ }
+ }
+ String log = new String(Files.readAllBytes(output), StandardCharsets.UTF_8);
+ assertTrue(finished, () -> "Timed out: " + command + "\n" + log);
+ assertEquals(0, process.exitValue(), () -> "Failed: " + command + "\n" + log);
+ assertFalse(log.contains("WARNING in native method"), log);
+ assertFalse(log.contains("FATAL ERROR in native method"), log);
+ System.out.print(log);
+ assumeFalse(log.contains("SKIP " + scenario), log);
+ assertTrue(log.contains("PASS " + scenario), log);
+ return log;
+ }
+}
diff --git a/affinity/src/test/java/net/openhft/affinity/LockCheckTest.java b/affinity/src/test/java/net/openhft/affinity/LockCheckTest.java
index eb4122858..8b450ef54 100644
--- a/affinity/src/test/java/net/openhft/affinity/LockCheckTest.java
+++ b/affinity/src/test/java/net/openhft/affinity/LockCheckTest.java
@@ -10,9 +10,11 @@
import static org.junit.jupiter.api.Assertions.*;
import java.io.File;
-import java.io.FileWriter;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
import static net.openhft.affinity.LockCheck.IS_LINUX;
import static org.junit.jupiter.api.Assumptions.*;
@@ -70,7 +72,8 @@ void shouldNotBlowUpIfPidFileIsCorrupt() throws Exception {
LockCheck.updateCpu(cpu, 0);
final File file = lockChecker.doToFile(cpu);
- try (final FileWriter writer = new FileWriter(file, false)) {
+ try (final OutputStreamWriter writer =
+ new OutputStreamWriter(new FileOutputStream(file, false), StandardCharsets.UTF_8)) {
writer.append("not a number\nnot a date");
}
diff --git a/affinity/src/test/java/net/openhft/affinity/NativeAffinityIntegrationTest.java b/affinity/src/test/java/net/openhft/affinity/NativeAffinityIntegrationTest.java
new file mode 100644
index 000000000..1dea66777
--- /dev/null
+++ b/affinity/src/test/java/net/openhft/affinity/NativeAffinityIntegrationTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0
+ */
+package net.openhft.affinity;
+
+import net.openhft.affinity.impl.LinuxJNAAffinity;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import software.chronicle.enterprise.internals.impl.NativeAffinity;
+
+import java.io.File;
+import java.util.BitSet;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+
+@EnabledOnOs(OS.LINUX)
+// Match the make-c profile's ARM32 exclusion; aarch64 remains eligible.
+@DisabledIfSystemProperty(named = "os.arch", matches = "(?i)arm")
+public class NativeAffinityIntegrationTest extends AffinityTestProcess {
+ @Test
+ void roundTripsShortMask() throws Exception {
+ runNativeProbe("short-mask");
+ }
+
+ @Test
+ void agreesWithJna() throws Exception {
+ runNativeProbe("jna-parity");
+ }
+
+ @Test
+ void failedSetReportsErrnoWithoutChangingMask() throws Exception {
+ runNativeProbe("failed-set");
+ }
+
+ @Test
+ void repeatedReadsPassJniChecks() throws Exception {
+ runNativeProbe("repeated-reads");
+ }
+
+ private void runNativeProbe(String scenario) throws Exception {
+ assumeFalse(System.getProperties().containsKey("dontMake"), "Native build explicitly disabled with dontMake");
+ File classes = new File(NativeAffinity.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+ assertTrue(new File(classes, System.mapLibraryName("CEInternals")).isFile(), "Build the JNI library before testing");
+ String libraryPath = "-Djava.library.path=" + classes.getAbsolutePath();
+ if (scenario.equals("jna-parity")) {
+ // Older third-party jnidispatch builds emit their own -Xcheck:jni warnings.
+ runProbe(NativeAffinityIntegrationTest.class, scenario, true, libraryPath);
+ } else {
+ // Check only this checkout's JNI library, with JNA absent from the child classpath.
+ runProbe(NativeAffinityIntegrationTest.class, scenario, false, "-Xcheck:jni", libraryPath);
+ }
+ }
+
+ public static void main(String[] args) {
+ String scenario = System.getProperty("affinity.test.scenario");
+ assertTrue(NativeAffinity.LOADED, "The library built by this checkout must load");
+ NativeAffinity nativeAffinity = NativeAffinity.INSTANCE;
+ BitSet original = nativeAffinity.getAffinity();
+ assertNotNull(original);
+ assertFalse(original.isEmpty());
+ try {
+ if (scenario.equals("short-mask") || scenario.equals("jna-parity")) {
+ BitSet single = new BitSet();
+ single.set(original.nextSetBit(0));
+ if (scenario.equals("short-mask") && single.toByteArray().length >= 128) {
+ System.out.println("SKIP short-mask: no permitted CPU fits in fewer than 128 bytes");
+ return;
+ }
+ nativeAffinity.setAffinity(single);
+ assertEquals(single, nativeAffinity.getAffinity());
+ if (scenario.equals("jna-parity")) {
+ assertTrue(LinuxJNAAffinity.LOADED);
+ assertEquals(single, LinuxJNAAffinity.INSTANCE.getAffinity());
+ LinuxJNAAffinity.INSTANCE.setAffinity(original);
+ assertEquals(original, nativeAffinity.getAffinity());
+ }
+ } else if (scenario.equals("failed-set")) {
+ RuntimeException failure = assertThrows(RuntimeException.class,
+ () -> nativeAffinity.setAffinity(new BitSet()));
+ assertTrue(failure.getMessage().contains("sched_setaffinity"), failure.getMessage());
+ assertTrue(failure.getMessage().contains("maskBytes=0"), failure.getMessage());
+ assertTrue(failure.getMessage().contains("errno=22"), failure.getMessage());
+ assertEquals(original, nativeAffinity.getAffinity());
+ } else if (scenario.equals("repeated-reads")) {
+ for (int i = 0; i < 10_000; i++) {
+ assertEquals(original, nativeAffinity.getAffinity());
+ }
+ } else {
+ fail("Unknown probe: " + scenario);
+ }
+ } finally {
+ nativeAffinity.setAffinity(original);
+ assertEquals(original, nativeAffinity.getAffinity(), "Restore the exact original permitted mask");
+ }
+ System.out.println("PASS " + scenario + " original=" + original);
+ }
+}