Skip to content

Commit

Permalink
Add support for a static native library search path (#92)
Browse files Browse the repository at this point in the history
- Add optional jssc.boot.library.path for native libs, helps with sandboxed environments
- Add unit tests for jssc.boot.library.path
- Bump maven-surefire-plugin to 3.0.0-M4
  • Loading branch information
tresf committed Aug 5, 2021
1 parent 2c09d72 commit 135350e
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 1 deletion.
6 changes: 6 additions & 0 deletions ant/build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<entry key="maven.nativelibdir.path" value="${maven.nativelibdir.path}"/>
<entry key="maven.assembly.id" value="${maven.assembly.id}"/>
<entry key="maven.test.skip" value="${maven.test.skip}"/>
<entry key="maven.exclude.tests" value="${maven.exclude.tests}"/>
</propertyfile>
</target>

Expand Down Expand Up @@ -193,6 +194,11 @@
</and>
</condition>

<!-- Skip ManualBootLibraryPathTest test if we're not building a native lib -->
<condition property="maven.exclude.tests" value="**/ManualBootLibraryPathTest.java" else="">
<equals arg1="${cmake.build.skip}" arg2="true"/>
</condition>

<!-- Summarize host/target -->
<echo level="info">Tests will run only if the TARGET and HOST match:${line.separator}${line.separator}</echo>
<echo level="info">TARGET: ${os.target.classifier}</echo>
Expand Down
8 changes: 7 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
<plugin.osmaven.version>1.7.0</plugin.osmaven.version>
<plugin.signature.version>1.1</plugin.signature.version>
<plugin.source.version>3.0.1</plugin.source.version>
<plugin.surfire.version>3.0.0-M3</plugin.surfire.version>
<plugin.surfire.version>3.0.0-M4</plugin.surfire.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -253,6 +253,12 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${plugin.surfire.version}</version>
<configuration>
<reuseForks>false</reuseForks>
<excludes>
<exclude>${maven.exclude.tests}</exclude>
</excludes>
</configuration>
</plugin>

<plugin>
Expand Down
74 changes: 74 additions & 0 deletions src/main/java/jssc/DefaultJniExtractorStub.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* License: https://opensource.org/licenses/BSD-3-Clause
*/
package jssc;

import org.scijava.nativelib.DefaultJniExtractor;
import org.scijava.nativelib.NativeLibraryUtil;

import java.io.File;
import java.io.IOException;

/**
* @author A. Tres Finocchiaro
*
* Stub <code>DefaultJniExtractor</code> class to allow native-lib-loader to conditionally
* use a statically defined native search path <code>bootPath</code> when provided.
*/
public class DefaultJniExtractorStub extends DefaultJniExtractor {
private File bootPath;

/**
* Default constructor
*/
public DefaultJniExtractorStub(Class libraryJarClass) throws IOException {
super(libraryJarClass);
}

/**
* Force native-lib-loader to first look in the location defined as <code>bootPath</code>
* prior to extracting a native library, useful for sandboxed environments.
* <code>
* NativeLoader.setJniExtractor(new DefaultJniExtractorStub(null, "/opt/nativelibs")));
* NativeLoader.loadLibrary("mylibrary");
* </code>
*/
public DefaultJniExtractorStub(Class libraryJarClass, String bootPath) throws IOException {
this(libraryJarClass);

if(bootPath != null) {
File bootTest = new File(bootPath);
if(bootTest.exists()) {
// assume a static, existing directory will contain the native libs
this.bootPath = bootTest;
} else {
System.err.println("WARNING " + DefaultJniExtractorStub.class.getCanonicalName() + ": Boot path " + bootPath + " not found, falling back to default extraction behavior.");
}
}
}

/**
* If a <code>bootPath</code> was provided to the constructor and exists,
* calculate the <code>File</code> path without any extraction logic.
*
* If a <code>bootPath</code> was NOT provided or does NOT exist, fallback on
* the default extraction behavior.
*/
@Override
public File extractJni(String libPath, String libName) throws IOException {
// Lie and pretend it's already extracted at the bootPath location
if(bootPath != null) {
return new File(bootPath, NativeLibraryUtil.getPlatformLibraryName(libName));
}
// Fallback on default behavior
return super.extractJni(libPath, libName);
}

@Override
public void extractRegistered() throws IOException {
if(bootPath != null) {
return; // no-op
}
super.extractRegistered();
}
}
7 changes: 7 additions & 0 deletions src/main/java/jssc/SerialNativeInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ else if(osName.equals("SunOS"))
else if(osName.equals("Mac OS X") || osName.equals("Darwin"))
osType = OS_MAC_OS_X;
try {
/**
* JSSC includes a small, platform-specific shared library and uses native-lib-loader for extraction.
* - First, native-lib-loader will attempt to load this library from the system library path.
* - Next, it will fallback to <code>jssc.boot.library.path</code>
* - Finally it will attempt to extract the library from from the jssc.jar file, and load it.
*/
NativeLoader.setJniExtractor(new DefaultJniExtractorStub(null, System.getProperty("jssc.boot.library.path")));
NativeLoader.loadLibrary("jssc");
} catch (IOException ioException) {
throw new UnsatisfiedLinkError("Could not load the jssc library: " + ioException.getMessage());
Expand Down
31 changes: 31 additions & 0 deletions src/test/java/jssc/bootpath/ManualBootLibraryPathFailedTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package jssc.bootpath;

import jssc.SerialNativeInterface;
import org.junit.Test;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
* Tests if a valid <code>jssc.boot.library.path</code> which does NOT contain a native library
* will predictably fail. This test can be run regardless of whether or not a native binary was
* created during the build process.
*
* TODO: This MUST be in its own class to run in a separate JVM (https://stackoverflow.com/questions/68657855)
* - JUnit does NOT currently offer JVM unloading between methods.
* - maven-surefire-plugin DOES offer JVM unloading between classes using <code>reuseForks=false</code>
* - Unloading is needed due to NativeLoader.loadLibrary(...) calls System.loadLibrary(...) which is static
*/
public class ManualBootLibraryPathFailedTest {
@Test
public void testBootPathOverride() {
String nativeLibDir = "/"; // This should be valid on all platforms
System.setProperty("jssc.boot.library.path", nativeLibDir);
try {
SerialNativeInterface.getNativeLibraryVersion();
fail("Library loading should fail if path provided exists but does not contain a native library");
} catch (UnsatisfiedLinkError ignore) {
assertTrue("Library loading failed as expected with an invalid jssc.boot.library.path", true);
}
}
}
35 changes: 35 additions & 0 deletions src/test/java/jssc/bootpath/ManualBootLibraryPathTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package jssc.bootpath;

import jssc.SerialNativeInterface;
import org.junit.Test;
import org.scijava.nativelib.NativeLibraryUtil;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

/**
* Tests if a valid <code>jssc.boot.library.path</code> which DOES contain a native library
* will predictably pass. This test can ONLY be run regardless if a native binary was created
* during the build process. See also <code>maven.exclude.tests</code>.
*
* TODO: This MUST be in its own class to run in a separate JVM (https://stackoverflow.com/questions/68657855)
* - JUnit does NOT currently offer JVM unloading between methods.
* - maven-surefire-plugin DOES offer JVM unloading between classes using <code>reuseForks=false</code>
* - Unloading is needed due to NativeLoader.loadLibrary(...) calls System.loadLibrary(...) which is static
*/
public class ManualBootLibraryPathTest {
@Test
public void testBootPathOverride() {
String nativeLibDir = NativeLibraryUtil.getPlatformLibraryPath(System.getProperty("user.dir") + "/target/cmake/natives/");
System.setProperty("jssc.boot.library.path", nativeLibDir);
try {
final String nativeLibraryVersion = SerialNativeInterface.getNativeLibraryVersion();
assertThat(nativeLibraryVersion, is(not(nullValue())));
assertThat(nativeLibraryVersion, is(not("")));
} catch (UnsatisfiedLinkError linkError) {
linkError.printStackTrace();
fail("Should be able to call method!");
}
}
}

0 comments on commit 135350e

Please sign in to comment.