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); + } +}