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("unchecked") 060 public static <T> List<String> extractLibraries(Class<T> clazz, String resourceName) 061 throws IOException { 062 TypeReference<HashMap<String, Object>> typeRef = new TypeReference<>() {}; 063 ObjectMapper mapper = new ObjectMapper(); 064 Map<String, Object> map; 065 try (var stream = clazz.getResourceAsStream(resourceName)) { 066 map = mapper.readValue(stream, typeRef); 067 } 068 069 var platformPath = Paths.get(RuntimeDetector.getPlatformPath()); 070 var platform = platformPath.getName(0).toString(); 071 var arch = platformPath.getName(1).toString(); 072 073 var platformMap = (Map<String, List<String>>) map.get(platform); 074 075 var fileList = platformMap.get(arch); 076 077 var extractionPathString = getExtractionDirectory(); 078 079 if (extractionPathString == null) { 080 String hash = (String) map.get("hash"); 081 082 var defaultExtractionRoot = RuntimeLoader.getDefaultExtractionRoot(); 083 var extractionPath = Paths.get(defaultExtractionRoot, platform, arch, hash); 084 extractionPathString = extractionPath.toString(); 085 086 setExtractionDirectory(extractionPathString); 087 } 088 089 List<String> extractedFiles = new ArrayList<>(); 090 091 byte[] buffer = new byte[0x10000]; // 64K copy buffer 092 093 for (var file : fileList) { 094 try (var stream = clazz.getResourceAsStream(file)) { 095 Objects.requireNonNull(stream); 096 097 var outputFile = Paths.get(extractionPathString, new File(file).getName()); 098 extractedFiles.add(outputFile.toString()); 099 if (outputFile.toFile().exists()) { 100 continue; 101 } 102 var parent = outputFile.getParent(); 103 if (parent == null) { 104 throw new IOException("Output file has no parent"); 105 } 106 parent.toFile().mkdirs(); 107 108 try (var os = Files.newOutputStream(outputFile)) { 109 int readBytes; 110 while ((readBytes = stream.read(buffer)) != -1) { // NOPMD 111 os.write(buffer, 0, readBytes); 112 } 113 } 114 } 115 } 116 117 return extractedFiles; 118 } 119 120 /** 121 * Load a single library from a list of extracted files. 122 * 123 * @param libraryName The library name to load 124 * @param extractedFiles The extracted files to search 125 * @throws IOException If library was not found 126 */ 127 public static void loadLibrary(String libraryName, List<String> extractedFiles) 128 throws IOException { 129 String currentPath = null; 130 String oldDllDirectory = null; 131 try { 132 if (RuntimeDetector.isWindows()) { 133 var extractionPathString = getExtractionDirectory(); 134 oldDllDirectory = setDllDirectory(extractionPathString); 135 } 136 for (var extractedFile : extractedFiles) { 137 if (extractedFile.contains(libraryName)) { 138 // Load it 139 currentPath = extractedFile; 140 System.load(extractedFile); 141 return; 142 } 143 } 144 throw new IOException("Could not find library " + libraryName); 145 } catch (UnsatisfiedLinkError ule) { 146 throw new IOException(getLoadErrorMessage(currentPath, ule)); 147 } finally { 148 if (oldDllDirectory != null) { 149 setDllDirectory(oldDllDirectory); 150 } 151 } 152 } 153 154 /** 155 * Load a list of native libraries out of a single directory. 156 * 157 * @param <T> The class where the resources would be located 158 * @param clazz The actual class object 159 * @param librariesToLoad List of libraries to load 160 * @throws IOException Throws an IOException if not found 161 */ 162 public static <T> void loadLibraries(Class<T> clazz, String... librariesToLoad) 163 throws IOException { 164 // Extract everything 165 166 var extractedFiles = extractLibraries(clazz, "/ResourceInformation.json"); 167 168 String currentPath = ""; 169 170 try { 171 if (RuntimeDetector.isWindows()) { 172 var extractionPathString = getExtractionDirectory(); 173 // Load windows, set dll directory 174 currentPath = Paths.get(extractionPathString, "WindowsLoaderHelper.dll").toString(); 175 System.load(currentPath); 176 } 177 } catch (UnsatisfiedLinkError ule) { 178 throw new IOException(getLoadErrorMessage(currentPath, ule)); 179 } 180 181 for (var library : librariesToLoad) { 182 loadLibrary(library, extractedFiles); 183 } 184 } 185}