001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.util; 006 007import java.io.File; 008import java.io.IOException; 009import java.io.InputStream; 010import java.io.OutputStream; 011import java.nio.charset.StandardCharsets; 012import java.nio.file.Files; 013import java.nio.file.Paths; 014import java.security.DigestInputStream; 015import java.security.MessageDigest; 016import java.security.NoSuchAlgorithmException; 017import java.util.Locale; 018import java.util.Scanner; 019 020public final class RuntimeLoader<T> { 021 private static String defaultExtractionRoot; 022 023 /** 024 * Gets the default extration root location (~/.wpilib/nativecache). 025 * 026 * @return The default extraction root location. 027 */ 028 public static synchronized String getDefaultExtractionRoot() { 029 if (defaultExtractionRoot != null) { 030 return defaultExtractionRoot; 031 } 032 String home = System.getProperty("user.home"); 033 defaultExtractionRoot = Paths.get(home, ".wpilib", "nativecache").toString(); 034 return defaultExtractionRoot; 035 } 036 037 private final String m_libraryName; 038 private final Class<T> m_loadClass; 039 private final String m_extractionRoot; 040 041 /** 042 * Creates a new library loader. 043 * 044 * @param libraryName Name of library to load. 045 * @param extractionRoot Location from which to load the library. 046 * @param cls Class whose classpath the given library belongs. 047 */ 048 public RuntimeLoader(String libraryName, String extractionRoot, Class<T> cls) { 049 m_libraryName = libraryName; 050 m_loadClass = cls; 051 m_extractionRoot = extractionRoot; 052 } 053 054 /** 055 * Returns a load error message given the information in the provided UnsatisfiedLinkError. 056 * 057 * @param ule UnsatisfiedLinkError object. 058 * @return A load error message. 059 */ 060 private String getLoadErrorMessage(UnsatisfiedLinkError ule) { 061 StringBuilder msg = new StringBuilder(512); 062 msg.append(m_libraryName) 063 .append( 064 " could not be loaded from path or an embedded resource.\n" 065 + "\tattempted to load for platform ") 066 .append(RuntimeDetector.getPlatformPath()) 067 .append("\nLast Load Error: \n") 068 .append(ule.getMessage()) 069 .append('\n'); 070 if (RuntimeDetector.isWindows()) { 071 msg.append( 072 "A common cause of this error is missing the C++ runtime.\n" 073 + "Download the latest at https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads\n"); 074 } 075 return msg.toString(); 076 } 077 078 /** 079 * Loads a native library. 080 * 081 * @throws IOException if the library fails to load 082 */ 083 @SuppressWarnings("PMD.PreserveStackTrace") 084 public void loadLibrary() throws IOException { 085 try { 086 // First, try loading path 087 System.loadLibrary(m_libraryName); 088 } catch (UnsatisfiedLinkError ule) { 089 // Then load the hash from the resources 090 String hashName = RuntimeDetector.getHashLibraryResource(m_libraryName); 091 String resname = RuntimeDetector.getLibraryResource(m_libraryName); 092 try (InputStream hashIs = m_loadClass.getResourceAsStream(hashName)) { 093 if (hashIs == null) { 094 throw new IOException(getLoadErrorMessage(ule)); 095 } 096 try (Scanner scanner = new Scanner(hashIs, StandardCharsets.UTF_8.name())) { 097 String hash = scanner.nextLine(); 098 File jniLibrary = new File(m_extractionRoot, resname + "." + hash); 099 try { 100 // Try to load from an already extracted hash 101 System.load(jniLibrary.getAbsolutePath()); 102 } catch (UnsatisfiedLinkError ule2) { 103 // If extraction failed, extract 104 try (InputStream resIs = m_loadClass.getResourceAsStream(resname)) { 105 if (resIs == null) { 106 throw new IOException(getLoadErrorMessage(ule)); 107 } 108 109 var parentFile = jniLibrary.getParentFile(); 110 if (parentFile == null) { 111 throw new IOException("JNI library has no parent file"); 112 } 113 parentFile.mkdirs(); 114 115 try (OutputStream os = Files.newOutputStream(jniLibrary.toPath())) { 116 byte[] buffer = new byte[0xFFFF]; // 64K copy buffer 117 int readBytes; 118 while ((readBytes = resIs.read(buffer)) != -1) { // NOPMD 119 os.write(buffer, 0, readBytes); 120 } 121 } 122 System.load(jniLibrary.getAbsolutePath()); 123 } 124 } 125 } 126 } 127 } 128 } 129 130 /** 131 * Load a native library by directly hashing the file. 132 * 133 * @throws IOException if the library failed to load 134 */ 135 @SuppressWarnings({"PMD.PreserveStackTrace", "PMD.EmptyWhileStmt"}) 136 public void loadLibraryHashed() throws IOException { 137 try { 138 // First, try loading path 139 System.loadLibrary(m_libraryName); 140 } catch (UnsatisfiedLinkError ule) { 141 // Then load the hash from the input file 142 String resname = RuntimeDetector.getLibraryResource(m_libraryName); 143 String hash; 144 try (InputStream is = m_loadClass.getResourceAsStream(resname)) { 145 if (is == null) { 146 throw new IOException(getLoadErrorMessage(ule)); 147 } 148 MessageDigest md; 149 try { 150 md = MessageDigest.getInstance("MD5"); 151 } catch (NoSuchAlgorithmException nsae) { 152 throw new RuntimeException("Weird Hash Algorithm?"); 153 } 154 try (DigestInputStream dis = new DigestInputStream(is, md)) { 155 // Read the entire buffer once to hash 156 byte[] buffer = new byte[0xFFFF]; 157 while (dis.read(buffer) > -1) {} 158 MessageDigest digest = dis.getMessageDigest(); 159 byte[] digestOutput = digest.digest(); 160 StringBuilder builder = new StringBuilder(); 161 for (byte b : digestOutput) { 162 builder.append(String.format("%02X", b)); 163 } 164 hash = builder.toString().toLowerCase(Locale.ENGLISH); 165 } 166 } 167 if (hash == null) { 168 throw new IOException("Weird Hash?"); 169 } 170 File jniLibrary = new File(m_extractionRoot, resname + "." + hash); 171 try { 172 // Try to load from an already extracted hash 173 System.load(jniLibrary.getAbsolutePath()); 174 } catch (UnsatisfiedLinkError ule2) { 175 // If extraction failed, extract 176 try (InputStream resIs = m_loadClass.getResourceAsStream(resname)) { 177 if (resIs == null) { 178 throw new IOException(getLoadErrorMessage(ule)); 179 } 180 181 var parentFile = jniLibrary.getParentFile(); 182 if (parentFile == null) { 183 throw new IOException("JNI library has no parent file"); 184 } 185 parentFile.mkdirs(); 186 187 try (OutputStream os = Files.newOutputStream(jniLibrary.toPath())) { 188 byte[] buffer = new byte[0xFFFF]; // 64K copy buffer 189 int readBytes; 190 while ((readBytes = resIs.read(buffer)) != -1) { // NOPMD 191 os.write(buffer, 0, readBytes); 192 } 193 } 194 System.load(jniLibrary.getAbsolutePath()); 195 } 196 } 197 } 198 } 199}