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 extraction 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 public void loadLibrary() throws IOException { 084 try { 085 // First, try loading path 086 System.loadLibrary(m_libraryName); 087 } catch (UnsatisfiedLinkError ule) { 088 // Then load the hash from the resources 089 String hashName = RuntimeDetector.getHashLibraryResource(m_libraryName); 090 String resName = RuntimeDetector.getLibraryResource(m_libraryName); 091 try (InputStream hashIs = m_loadClass.getResourceAsStream(hashName)) { 092 if (hashIs == null) { 093 throw new IOException(getLoadErrorMessage(ule)); 094 } 095 try (Scanner scanner = new Scanner(hashIs, StandardCharsets.UTF_8)) { 096 String hash = scanner.nextLine(); 097 File jniLibrary = new File(m_extractionRoot, resName + "." + hash); 098 try { 099 // Try to load from an already extracted hash 100 System.load(jniLibrary.getAbsolutePath()); 101 } catch (UnsatisfiedLinkError ule2) { 102 // If extraction failed, extract 103 try (InputStream resIs = m_loadClass.getResourceAsStream(resName)) { 104 if (resIs == null) { 105 throw new IOException(getLoadErrorMessage(ule)); 106 } 107 108 var parentFile = jniLibrary.getParentFile(); 109 if (parentFile == null) { 110 throw new IOException("JNI library has no parent file"); 111 } 112 parentFile.mkdirs(); 113 114 try (OutputStream os = Files.newOutputStream(jniLibrary.toPath())) { 115 byte[] buffer = new byte[0xFFFF]; // 64K copy buffer 116 int readBytes; 117 while ((readBytes = resIs.read(buffer)) != -1) { // NOPMD 118 os.write(buffer, 0, readBytes); 119 } 120 } 121 System.load(jniLibrary.getAbsolutePath()); 122 } 123 } 124 } 125 } 126 } 127 } 128 129 /** 130 * Load a native library by directly hashing the file. 131 * 132 * @throws IOException if the library failed to load 133 */ 134 public void loadLibraryHashed() throws IOException { 135 try { 136 // First, try loading path 137 System.loadLibrary(m_libraryName); 138 } catch (UnsatisfiedLinkError ule) { 139 // Then load the hash from the input file 140 String resName = RuntimeDetector.getLibraryResource(m_libraryName); 141 String hash; 142 try (InputStream is = m_loadClass.getResourceAsStream(resName)) { 143 if (is == null) { 144 throw new IOException(getLoadErrorMessage(ule)); 145 } 146 MessageDigest md; 147 try { 148 md = MessageDigest.getInstance("MD5"); 149 } catch (NoSuchAlgorithmException nsae) { 150 throw new RuntimeException("Weird Hash Algorithm?"); 151 } 152 try (DigestInputStream dis = new DigestInputStream(is, md)) { 153 // Read the entire buffer once to hash 154 byte[] buffer = new byte[0xFFFF]; 155 while (dis.read(buffer) > -1) {} 156 MessageDigest digest = dis.getMessageDigest(); 157 byte[] digestOutput = digest.digest(); 158 StringBuilder builder = new StringBuilder(); 159 for (byte b : digestOutput) { 160 builder.append(String.format("%02X", b)); 161 } 162 hash = builder.toString().toLowerCase(Locale.ENGLISH); 163 } 164 } 165 if (hash == null) { 166 throw new IOException("Weird Hash?"); 167 } 168 File jniLibrary = new File(m_extractionRoot, resName + "." + hash); 169 try { 170 // Try to load from an already extracted hash 171 System.load(jniLibrary.getAbsolutePath()); 172 } catch (UnsatisfiedLinkError ule2) { 173 // If extraction failed, extract 174 try (InputStream resIs = m_loadClass.getResourceAsStream(resName)) { 175 if (resIs == null) { 176 throw new IOException(getLoadErrorMessage(ule)); 177 } 178 179 var parentFile = jniLibrary.getParentFile(); 180 if (parentFile == null) { 181 throw new IOException("JNI library has no parent file"); 182 } 183 parentFile.mkdirs(); 184 185 try (OutputStream os = Files.newOutputStream(jniLibrary.toPath())) { 186 byte[] buffer = new byte[0xFFFF]; // 64K copy buffer 187 int readBytes; 188 while ((readBytes = resIs.read(buffer)) != -1) { // NOPMD 189 os.write(buffer, 0, readBytes); 190 } 191 } 192 System.load(jniLibrary.getAbsolutePath()); 193 } 194 } 195 } 196 } 197}