zaber.jne.JNE Maven / Gradle / Ivy
Show all versions of motion-library Show documentation
package zaber.jne;
/*-
* #%L
* jne
* %%
* Copyright (C) 2016 - 2017 Fizzed, Inc
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class JNE {
static public Options DEFAULT_OPTIONS = new Options();
static private File TEMP_DIRECTORY;
static private final ConcurrentHashMap JAR_VERSION_HASHES = new ConcurrentHashMap<>();
/**
* Finds (extracts if necessary) a named executable for the runtime
* operating system and architecture. The executable should be a regular
* Java resource at the path /jne/[os]/[arch]/[exe]. The name of the file
* will be automatically adjusted for the target platform. For example, on
* Windows, to find the "cat" application, this method will actually search
* for "cat.exe".
*
* @param name The executable name you would normally type on the
* command-line. For example, "cat" or "ping" would search for "ping.exe" on
* windows and "ping" on linux/mac.
* @return The executable file or null if no executable found.
* @throws java.io.IOException
* @throws ExtractException Thrown if a runtime exception occurs while
* finding or extracting the executable.
*/
synchronized static public File findExecutable(String name) throws IOException {
return findExecutable(name, null, null);
}
/**
* Finds (extracts if necessary) a named executable for the runtime
* operating system and architecture. The executable should be a regular
* Java resource at the path /jne/[os]/[arch]/[exe]. The name of the file
* will be automatically adjusted for the target platform. For example, on
* Windows, to find the "cat" application, this method will actually search
* for "cat.exe".
*
* @param name The executable name you would normally type on the
* command-line. For example, "cat" or "ping" would search for "ping.exe" on
* windows and "ping" on linux/mac.
* @param targetName The executable name you would like the resource (if
* found) to be named on extract.
* @return The executable file or null if no executable found.
* @throws java.io.IOException
* @throws ExtractException Thrown if a runtime exception occurs while
* finding or extracting the executable.
*/
synchronized static public File findExecutable(String name, String targetName) throws IOException {
return findExecutable(name, targetName, null);
}
/**
* Finds (or extracts) a named executable for the runtime operating system
* and architecture. The executable should be a regular Java resource at the
* path /jne/[os]/[arch]/[exe].
*
* @param name The executable name you would normally type on the
* command-line. For example, "cat" or "ping" would search for "ping.exe" on
* windows and "ping" on linux/mac.
* @param options The options to use when finding an executable. If null
* then the default options will be used.
* @return The executable file or null if no executable found.
* @throws java.io.IOException
* @throws ExtractException Thrown if a runtime exception occurs while
* finding or extracting the executable.
*/
synchronized static public File findExecutable(String name, Options options) throws IOException {
return findExecutable(name, null, options);
}
/**
* Finds (or extracts) a named executable for the runtime operating system
* and architecture. The executable should be a regular Java resource at the
* path /jne/[os]/[arch]/[exe].
*
* @param name The executable name you would normally type on the
* command-line. For example, "cat" or "ping" would search for "ping.exe" on
* windows and "ping" on linux/mac.
* @param targetName The executable name you would like the resource (if
* found) to be named on extract.
* @param options The options to use when finding an executable. If null
* then the default options will be used.
* @return The executable file or null if no executable found.
* @throws java.io.IOException
* @throws ExtractException Thrown if a runtime exception occurs while
* finding or extracting the executable.
*/
synchronized static public File findExecutable(String name, String targetName, Options options) throws IOException {
if (options == null) {
options = DEFAULT_OPTIONS;
}
String fileName = options.createExecutableName(name, options.getOperatingSystem());
String targetFileName = null;
if (targetName != null) {
targetFileName = options.createExecutableName(targetName, options.getOperatingSystem());
}
// always search for specific arch first
File file = find(fileName, targetFileName, options, options.getOperatingSystem(), options.getHardwareArchitecture());
// for x64 fallback to x86 if an exe was not found
if (file == null && options.isX32ExecutableFallback() && options.getHardwareArchitecture() == HardwareArchitecture.X64) {
file = find(fileName, targetFileName, options, options.getOperatingSystem(), HardwareArchitecture.X32);
}
return file;
}
/**
* Same as findExecutable but throws an exception if the executable was not
* found.
*/
synchronized static public File requireExecutable(String name) throws IOException {
return requireExecutable(name, null, null);
}
/**
* Same as findExecutable but throws an exception if the executable was not
* found.
*/
synchronized static public File requireExecutable(String name, Options options) throws IOException {
return requireExecutable(name, null, options);
}
/**
* Same as findExecutable but throws an exception if the executable was not
* found.
*/
synchronized static public File requireExecutable(String name, String targetName, Options options) throws IOException {
File file = findExecutable(name, targetName, options);
if (file == null) {
throw new ResourceNotFoundException("Resource executable " + name + " not found");
}
return file;
}
synchronized static public File findLibrary(String name) {
return findLibrary(name, null, null);
}
synchronized static public File findLibrary(String name, Integer majorVersion) {
return findLibrary(name, null, majorVersion);
}
synchronized static public File findLibrary(String name, Options options, Integer majorVersion) {
if (options == null) {
options = DEFAULT_OPTIONS;
}
// file name to try and find/extract (only support major for now since linux 99% of the time links to major)
String fileName = options.createLibraryName(name, options.getOperatingSystem(), majorVersion, null, null);
try {
// always search for specific arch first
return find(fileName, null, options, options.getOperatingSystem(), options.getHardwareArchitecture());
} catch (IOException e) {
throw new UnsatisfiedLinkError(e.getMessage());
}
}
/**
*
* Loads a dynamic library. Attempts to find (extracts if necessary) a named
* library for the runtime operating system and architecture. If the library
* was found as a resource and/or extracted, it will then be loaded via
* System.load(). If the library was not found as a resource, this method
* will simply fallback to System.loadLibrary(). Thus, this method should be
* safe as a drop-in replacement for calls to System.loadLibrary().
*
*
* If including the library as a Java resource, the resource path will be
* /jne/[os]/[arch]/[lib]. The name of the file will be automatically
* adjusted for the target platform. For example, on Windows, to find the
* "cat" library, this method will search for "cat.dll". On Linux, to find
* the "cat" library, this method will search for "libcat.so". On Mac, to
* find the "cat" library, this method will search for "libcat.dylib".
*
*
* @param name The library name to find and load
* @throws UnsatisfiedLinkError Thrown if a runtime exception occurs while
* finding or extracting the executable.
*/
synchronized static public void loadLibrary(String name) {
loadLibrary(name, null, null);
}
synchronized static public void loadLibrary(String name, Integer majorVersion) {
loadLibrary(name, null, majorVersion);
}
/**
*
* Loads a dynamic library. Attempts to find (extracts if necessary) a named
* library for the runtime operating system and architecture. If the library
* was found as a resource and/or extracted, it will then be loaded via
* System.load(). If the library was not found as a resource, this method
* will simply fallback to System.loadLibrary(). Thus, this method should be
* safe as a drop-in replacement for calls to System.loadLibrary().
*
*
* If including the library as a Java resource, the resource path will be
* /jne/[os]/[arch]/[lib]. The name of the file will be automatically
* adjusted for the target platform. For example, on Windows, to find the
* "cat" library, this method will search for "cat.dll". On Linux, to find
* the "cat" library, this method will search for "libcat.so". On Mac, to
* find the "cat" library, this method will search for "libcat.dylib".
*
*
* @param name The library name to find and load
* @param options The options to use when finding the library. If null then
* the default options will be used.
* @param majorVersion
* @throws UnsatisfiedLinkError Thrown if a runtime exception occurs while
* finding or extracting the executable.
*/
synchronized static public void loadLibrary(String name, Options options, Integer majorVersion) {
// search for specific library
File f = null;
try {
f = findLibrary(name, options, majorVersion);
} catch (Exception e) {
throw new UnsatisfiedLinkError("Unable to cleanly find (or extract) library [" + name + "] as resource");
}
// temporarily prepend library path to load library if found
if (f != null) {
// since loading of dependencies of a library cannot dynamically happen
// and the user would be required to provide a valid LD_LIBRARY_PATH when
// launching the java process -- we don't need to do use loadLibrary
// and can just tell it to load a specific library file
System.load(f.getAbsolutePath());
} else {
// fallback to java method
System.loadLibrary(name);
}
}
/**
* Finds (or extracts) a named file. Will first attempt to locate the file
* for the runtime operating system and architecture, then fallback to just
* the runtime operating system, and finally fallback to the resource
* prefix. For example, a file named "resource.txt" running on a JVM on x64
* linux would search the following 3 resource paths:
*
* /jne/linux/x64/resource.txt /jne/linux/resource.txt /jne/resource.txt
*
* @param name The file name to find or extract.
* @return The file or null if no file found.
* @throws java.io.IOException
* @throws ExtractException Thrown if a runtime exception occurs while
* finding or extracting the executable.
*/
synchronized static public File findFile(String name) throws IOException {
return JNE.findFile(name, null);
}
/**
* Finds (or extracts) a named file. Will first attempt to locate the file
* for the runtime operating system and architecture, then fallback to just
* the runtime operating system, and finally fallback to the resource
* prefix. For example, a file named "resource.txt" running on a JVM on x64
* linux would search the following 3 resource paths:
*
* /jne/linux/x64/resource.txt /jne/linux/resource.txt /jne/resource.txt
*
* @param name The file name to find or extract.
* @param options The options to use when finding an executable. If null
* then the default options will be used.
* @return The file or null if no file found.
* @throws java.io.IOException
* @throws ExtractException Thrown if a runtime exception occurs while
* finding or extracting the executable.
*/
synchronized static public File findFile(String name, Options options) throws IOException {
if (options == null) {
options = DEFAULT_OPTIONS;
}
// 1. try with os & arch
File file = JNE.find(name, name, options, options.getOperatingSystem(), options.getHardwareArchitecture());
// 2. try with os & any arch
if (file == null) {
file = JNE.find(name, name, options, options.getOperatingSystem(), HardwareArchitecture.ANY);
}
// 3. try with os & any arch
if (file == null) {
file = JNE.find(name, name, options, OperatingSystem.ANY, HardwareArchitecture.ANY);
}
return file;
}
/**
* Same as findFile but throws an exception if the file was not found.
*/
synchronized static public File requireFile(String name) throws IOException {
return JNE.requireFile(name, null);
}
/**
* Same as findFile but throws an exception if the file was not found.
*/
synchronized static public File requireFile(String name, Options options) throws IOException {
File file = findFile(name, options);
if (file == null) {
throw new ResourceNotFoundException("Resource file " + name + " not found");
}
return file;
}
/**
* Underlying method used by findExecutable and loadLibrary to find and
* extract executables as needed. Although public, it's NOT recommended to
* use this method unless you know what you're doing.
*
* @param fileName
* @param targetFileName
* @param options
* @param os
* @param arch
* @return
* @throws IOException
* @throws ExtractException
*/
synchronized static public File find(String fileName, String targetFileName, Options options, OperatingSystem os, HardwareArchitecture arch) throws IOException {
if (options == null) {
options = DEFAULT_OPTIONS;
}
if (os == null || os == OperatingSystem.UNKNOWN) {
throw new ExtractException("Unable to detect operating system (e.g. Windows)");
}
if (arch == null || arch == HardwareArchitecture.UNKNOWN) {
throw new ExtractException("Unable to detect hardware architecture (e.g. x86)");
}
if (targetFileName == null) {
targetFileName = fileName;
}
//String resourcePath = options.getResourcePrefix() + "/" + os.name().toLowerCase() + "/" + arch.name().toLowerCase() + "/" + name;
String resourcePath = options.createResourcePath(os, arch, fileName);
URL url = JNE.class.getResource(resourcePath);
if (url == null) {
return null;
}
// support for "file" and "jar"
if (url.getProtocol().equals("jar")) {
// in the case of where the app specifies an extract directory and
// does not request deleteOnExit we need a way to detect if the
// executables changed from the previous app run -- we do this with
// a very basic "hash" for an extracted resource. We basically combine
// the path of the jar and manifest version of when the exe was extracted
String versionHash = getJarVersionHashForResource(url);
// where should we extract the executable?
File d = options.getExtractDir();
if (d == null) {
d = getOrCreateTempDirectory(options.isCleanupExtracted());
} else {
// does the extract dir exist?
if (!d.exists()) {
d.mkdirs();
}
if (!d.isDirectory()) {
throw new ExtractException("Extract dir [" + d + "] is not a directory");
}
}
// create both target exe and hash files
File exeFile = new File(d, targetFileName);
File exeHashFile = new File(exeFile.getAbsolutePath() + ".hash");
// if file already exists verify its hash
if (exeFile.exists()) {
// verify the version hash still matches
if (!exeHashFile.exists()) {
// hash file missing -- we will force a new extract to be safe
exeFile.delete();
} else {
// hash file exists, verify it matches what we expect
String existingHash = readFileToString(exeHashFile);
if (existingHash == null || !existingHash.equals(versionHash)) {
// hash mismatch -- will force an overwrite of both files
exeFile.delete();
exeHashFile.delete();
} else {
// hash match (exeFile and exeHashFile are both perrrrfect)
//System.out.println("exe already extracted AND hash matched -- reusing same exe");
return exeFile;
}
}
}
// does exe already exist? (previously extracted)
if (!exeFile.exists()) {
try {
extractTo(url, exeFile);
// set file to "executable"
exeFile.setExecutable(true);
// create corrosponding hash file
writeStringToFile(exeHashFile, versionHash);
// schedule files for deletion?
if (options.isCleanupExtracted()) {
exeFile.deleteOnExit();
exeHashFile.deleteOnExit();
}
} catch (IOException e) {
throw new ExtractException("Unable to cleanly extract executable from jar", e);
}
}
return exeFile;
} else if (url.getProtocol().equals("file")) {
try {
File exeFile = new File(url.toURI());
if (!exeFile.canExecute()) {
if (!exeFile.setExecutable(true)) {
throw new ExtractException("Executable was found but it cannot be set to execute [" + exeFile.getAbsolutePath() + "]");
}
}
return exeFile;
} catch (URISyntaxException e) {
throw new ExtractException("Unable to create executable file from uri", e);
}
} else {
throw new ExtractException("Unsupported executable resource protocol [" + url.getProtocol() + "]");
}
}
static private void extractTo(URL url, File file) throws IOException {
final InputStream in = url.openStream();
try {
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(file, false))) {
int len;
byte[] buffer = new byte[8192];
while ((len = in.read(buffer)) > -1) {
out.write(buffer, 0, len);
}
out.flush();
}
} finally {
if (in != null) {
in.close();
}
}
}
static private String readFileToString(File file) throws IOException {
StringBuilder result = new StringBuilder();
try (BufferedInputStream is = new BufferedInputStream(new FileInputStream(file))) {
byte[] buf = new byte[1024];
int len;
while ((len = is.read(buf)) > -1) {
result.append(new String(buf, 0, len, "UTF-8"));
}
}
return result.toString();
}
static private void writeStringToFile(File file, String s) throws IOException {
try (FileOutputStream os = new FileOutputStream(file, false)) {
os.write(s.getBytes("UTF-8"));
os.flush();
}
}
static private String getJarVersionHashForResource(URL resource) throws IOException {
// get the file that points to the underlying jar for this resource
File jarFile = JarUtil.getJarFileForResource(resource);
if (JAR_VERSION_HASHES.containsKey(jarFile)) {
return JAR_VERSION_HASHES.get(jarFile);
} else {
// calculate new hash for jar
String manifestVersion = JarUtil.getManifestVersionNumber(jarFile);
StringBuilder hashBuilder = new StringBuilder();
hashBuilder.append("file:");
hashBuilder.append(jarFile.getAbsolutePath());
hashBuilder.append("|last_modified:");
hashBuilder.append(jarFile.lastModified());
hashBuilder.append("|version:");
hashBuilder.append(manifestVersion);
String hash = hashBuilder.toString();
JAR_VERSION_HASHES.put(jarFile, hash);
return hash;
}
}
/**
* Attempts to create a temporary directory that did not exist previously.
*/
static private File getOrCreateTempDirectory(boolean deleteOnExit) throws ExtractException {
// return the single instance if already created
if ((TEMP_DIRECTORY != null) && TEMP_DIRECTORY.exists()) {
return TEMP_DIRECTORY;
}
// use totally unique name to avoid race conditions
try {
Path baseDir = Paths.get(System.getProperty("java.io.tmpdir"));
Path tempDirectory = baseDir.resolve("jne." + UUID.randomUUID().toString());
Files.createDirectories(tempDirectory);
File tempDirectoryAsFile = tempDirectory.toFile();
if (deleteOnExit) {
tempDirectoryAsFile.deleteOnExit();
}
// save temp directory so its only exactracted once
TEMP_DIRECTORY = tempDirectoryAsFile;
return TEMP_DIRECTORY;
} catch (IOException e) {
throw new ExtractException("Unable to create temporary dir", e);
}
}
}