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 com.fasterxml.jackson.core.type.TypeReference; 008import com.fasterxml.jackson.databind.ObjectMapper; 009import java.io.File; 010import java.io.IOException; 011import java.nio.file.Files; 012import java.nio.file.Paths; 013import java.util.ArrayList; 014import java.util.HashMap; 015import java.util.List; 016import java.util.Map; 017import java.util.Objects; 018 019public final class CombinedRuntimeLoader { 020 private CombinedRuntimeLoader() {} 021 022 private static String extractionDirectory; 023 024 public static synchronized String getExtractionDirectory() { 025 return extractionDirectory; 026 } 027 028 private static synchronized void setExtractionDirectory(String directory) { 029 extractionDirectory = directory; 030 } 031 032 public static native String setDllDirectory(String directory); 033 034 private static String getLoadErrorMessage(String libraryName, UnsatisfiedLinkError ule) { 035 StringBuilder msg = new StringBuilder(512); 036 msg.append(libraryName) 037 .append(" could not be loaded from path\n" + "\tattempted to load for platform ") 038 .append(RuntimeDetector.getPlatformPath()) 039 .append("\nLast Load Error: \n") 040 .append(ule.getMessage()) 041 .append('\n'); 042 if (RuntimeDetector.isWindows()) { 043 msg.append( 044 "A common cause of this error is missing the C++ runtime.\n" 045 + "Download the latest at https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads\n"); 046 } 047 return msg.toString(); 048 } 049 050 /** 051 * Extract a list of native libraries. 052 * 053 * @param <T> The class where the resources would be located 054 * @param clazz The actual class object 055 * @param resourceName The resource name on the classpath to use for file lookup 056 * @return List of all libraries that were extracted 057 * @throws IOException Thrown if resource not found or file could not be extracted 058 */ 059 @SuppressWarnings({"PMD.UnnecessaryCastRule", "unchecked"}) 060 public static <T> List<String> extractLibraries(Class<T> clazz, String resourceName) 061 throws IOException { 062 TypeReference<HashMap<String, Object>> typeRef = 063 new TypeReference<HashMap<String, Object>>() {}; 064 ObjectMapper mapper = new ObjectMapper(); 065 Map<String, Object> map; 066 try (var stream = clazz.getResourceAsStream(resourceName)) { 067 map = mapper.readValue(stream, typeRef); 068 } 069 070 var platformPath = Paths.get(RuntimeDetector.getPlatformPath()); 071 var platform = platformPath.getName(0).toString(); 072 var arch = platformPath.getName(1).toString(); 073 074 var platformMap = (Map<String, List<String>>) map.get(platform); 075 076 var fileList = platformMap.get(arch); 077 078 var extractionPathString = getExtractionDirectory(); 079 080 if (extractionPathString == null) { 081 String hash = (String) map.get("hash"); 082 083 var defaultExtractionRoot = RuntimeLoader.getDefaultExtractionRoot(); 084 var extractionPath = Paths.get(defaultExtractionRoot, platform, arch, hash); 085 extractionPathString = extractionPath.toString(); 086 087 setExtractionDirectory(extractionPathString); 088 } 089 090 List<String> extractedFiles = new ArrayList<>(); 091 092 byte[] buffer = new byte[0x10000]; // 64K copy buffer 093 094 for (var file : fileList) { 095 try (var stream = clazz.getResourceAsStream(file)) { 096 Objects.requireNonNull(stream); 097 098 var outputFile = Paths.get(extractionPathString, new File(file).getName()); 099 extractedFiles.add(outputFile.toString()); 100 if (outputFile.toFile().exists()) { 101 continue; 102 } 103 var parent = outputFile.getParent(); 104 if (parent == null) { 105 throw new IOException("Output file has no parent"); 106 } 107 parent.toFile().mkdirs(); 108 109 try (var os = Files.newOutputStream(outputFile)) { 110 int readBytes; 111 while ((readBytes = stream.read(buffer)) != -1) { // NOPMD 112 os.write(buffer, 0, readBytes); 113 } 114 } 115 } 116 } 117 118 return extractedFiles; 119 } 120 121 /** 122 * Load a single library from a list of extracted files. 123 * 124 * @param libraryName The library name to load 125 * @param extractedFiles The extracted files to search 126 * @throws IOException If library was not found 127 */ 128 public static void loadLibrary(String libraryName, List<String> extractedFiles) 129 throws IOException { 130 String currentPath = null; 131 String oldDllDirectory = null; 132 try { 133 if (RuntimeDetector.isWindows()) { 134 var extractionPathString = getExtractionDirectory(); 135 oldDllDirectory = setDllDirectory(extractionPathString); 136 } 137 for (var extractedFile : extractedFiles) { 138 if (extractedFile.contains(libraryName)) { 139 // Load it 140 currentPath = extractedFile; 141 System.load(extractedFile); 142 return; 143 } 144 } 145 throw new IOException("Could not find library " + libraryName); 146 } catch (UnsatisfiedLinkError ule) { 147 throw new IOException(getLoadErrorMessage(currentPath, ule)); 148 } finally { 149 if (oldDllDirectory != null) { 150 setDllDirectory(oldDllDirectory); 151 } 152 } 153 } 154 155 /** 156 * Load a list of native libraries out of a single directory. 157 * 158 * @param <T> The class where the resources would be located 159 * @param clazz The actual class object 160 * @param librariesToLoad List of libraries to load 161 * @throws IOException Throws an IOException if not found 162 */ 163 public static <T> void loadLibraries(Class<T> clazz, String... librariesToLoad) 164 throws IOException { 165 // Extract everything 166 167 var extractedFiles = extractLibraries(clazz, "/ResourceInformation.json"); 168 169 String currentPath = ""; 170 171 try { 172 if (RuntimeDetector.isWindows()) { 173 var extractionPathString = getExtractionDirectory(); 174 // Load windows, set dll directory 175 currentPath = Paths.get(extractionPathString, "WindowsLoaderHelper.dll").toString(); 176 System.load(currentPath); 177 } 178 } catch (UnsatisfiedLinkError ule) { 179 throw new IOException(getLoadErrorMessage(currentPath, ule)); 180 } 181 182 for (var library : librariesToLoad) { 183 loadLibrary(library, extractedFiles); 184 } 185 } 186}