001/* 002 * Copyright (c) FIRST and other WPILib contributors. 003 * Open Source Software; you can modify and/or share it under the terms of 004 * the WPILib BSD license below: 005 * 006 * Redistribution and use in source and binary forms, with or without 007 * modification, are permitted provided that the following conditions 008 * are met: 009 * Redistributions of source code must retain the above copyright notice, 010 * this list of conditions and the following disclaimer. 011 * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * Neither the name of FIRST, WPILib, nor the names of other WPILib 015 * contributors may be used to endorse or promote products derived from 016 * this software without specific prior written permission. 017 */ 018 019package org.photonvision.jni; 020 021import io.avaje.jsonb.Json; 022import io.avaje.jsonb.JsonType; 023import io.avaje.jsonb.Jsonb; 024import java.io.BufferedInputStream; 025import java.io.File; 026import java.io.FileInputStream; 027import java.io.IOException; 028import java.nio.file.Files; 029import java.nio.file.Paths; 030import java.nio.file.StandardCopyOption; 031import java.security.DigestInputStream; 032import java.security.MessageDigest; 033import java.security.NoSuchAlgorithmException; 034import java.util.ArrayList; 035import java.util.HexFormat; 036import java.util.List; 037import java.util.Map; 038import java.util.Objects; 039import java.util.concurrent.CopyOnWriteArrayList; 040import org.photonvision.common.hardware.Platform; 041 042/** Loads dynamic libraries for all platforms. */ 043public final class CombinedRuntimeLoader { 044 private CombinedRuntimeLoader() {} 045 046 private static String extractionDirectory; 047 048 private static final Object extractCompleteLock = new Object(); 049 private static boolean extractAndVerifyComplete = false; 050 private static List<String> filesAlreadyExtracted = new CopyOnWriteArrayList<>(); 051 052 /** 053 * Returns library extraction directory. 054 * 055 * @return Library extraction directory. 056 */ 057 public static synchronized String getExtractionDirectory() { 058 return extractionDirectory; 059 } 060 061 private static synchronized void setExtractionDirectory(String directory) { 062 extractionDirectory = directory; 063 } 064 065 private static String defaultExtractionRoot; 066 067 /** 068 * Gets the default extraction root location (~/.wpilib/nativecache) for use if 069 * setExtractionDirectory is not set. 070 * 071 * @return The default extraction root location. 072 */ 073 public static synchronized String getDefaultExtractionRoot() { 074 if (defaultExtractionRoot != null) { 075 return defaultExtractionRoot; 076 } 077 String home = System.getProperty("user.home"); 078 defaultExtractionRoot = Paths.get(home, ".wpilib", "nativecache").toString(); 079 return defaultExtractionRoot; 080 } 081 082 /** 083 * Returns platform path. 084 * 085 * @return The current platform path. 086 * @throws IllegalStateException Thrown if the operating system is unknown. 087 */ 088 public static String getPlatformPath() { 089 String filePath; 090 String arch = System.getProperty("os.arch"); 091 092 boolean intel32 = "x86".equals(arch) || "i386".equals(arch); 093 boolean intel64 = "amd64".equals(arch) || "x86_64".equals(arch); 094 095 if (System.getProperty("os.name").startsWith("Windows")) { 096 if (intel32) { 097 filePath = "/windows/x86/"; 098 } else { 099 filePath = "/windows/x86-64/"; 100 } 101 } else if (System.getProperty("os.name").startsWith("Mac")) { 102 filePath = "/osx/universal/"; 103 } else if (System.getProperty("os.name").startsWith("Linux")) { 104 if (intel32) { 105 filePath = "/linux/x86/"; 106 } else if (intel64) { 107 filePath = "/linux/x86-64/"; 108 } else if (Platform.isAthena()) { 109 filePath = "/linux/athena/"; 110 } else if ("arm".equals(arch) || "arm32".equals(arch)) { 111 filePath = "/linux/arm32/"; 112 } else if ("aarch64".equals(arch) || "arm64".equals(arch)) { 113 filePath = "/linux/arm64/"; 114 } else { 115 filePath = "/linux/nativearm/"; 116 } 117 } else { 118 throw new IllegalStateException(); 119 } 120 121 return filePath; 122 } 123 124 private static String getLoadErrorMessage(String libraryName, UnsatisfiedLinkError ule) { 125 StringBuilder msg = new StringBuilder(512); 126 msg.append(libraryName) 127 .append(" could not be loaded from path\n" + "\tattempted to load for platform ") 128 .append(getPlatformPath()) 129 .append("\nLast Load Error: \n") 130 .append(ule.getMessage()) 131 .append('\n'); 132 if (System.getProperty("os.name").startsWith("Windows")) { 133 msg.append( 134 "A common cause of this error is missing the C++ runtime.\n" 135 + "Download the latest at https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads\n"); 136 } 137 return msg.toString(); 138 } 139 140 /** 141 * Architecture-specific information containing file hashes for a specific CPU architecture (e.g., 142 * x86-64, arm64). 143 */ 144 @Json 145 public record ArchInfo(Map<String, String> fileHashes) {} 146 147 /** 148 * Platform-specific information containing architectures for a specific OS platform (e.g., linux, 149 * windows). 150 */ 151 @Json 152 public record PlatformInfo(Map<String, ArchInfo> architectures) {} 153 154 /** Overall resource information to be serialized */ 155 @Json 156 public record ResourceInformation( 157 // Combined MD5 hash of all native resource files 158 String hash, 159 // Platform-specific native libraries organized by platform then architecture 160 Map<String, PlatformInfo> platforms, 161 // List of supported versions for these native resources 162 List<String> versions) {} 163 164 /** 165 * Extract a list of native libraries. 166 * 167 * @param <T> The class where the resources would be located 168 * @param clazz The actual class object 169 * @param resourceName The resource name on the classpath to use for file lookup 170 * @return List of all libraries that were extracted 171 * @throws IOException Thrown if resource not found or file could not be extracted 172 */ 173 public static <T> List<String> extractLibraries(Class<T> clazz, String resourceName) 174 throws IOException { 175 JsonType<ResourceInformation> jsonb = Jsonb.instance().type(ResourceInformation.class); 176 ResourceInformation resourceInfo; 177 try (var stream = clazz.getResourceAsStream(resourceName)) { 178 resourceInfo = jsonb.fromJson(stream); 179 } 180 181 var platformPath = Paths.get(getPlatformPath()); 182 var platform = platformPath.getName(0).toString(); 183 var arch = platformPath.getName(1).toString(); 184 185 var platformInfo = resourceInfo.platforms().get(platform); 186 if (platformInfo == null) { 187 throw new IOException("Platform " + platform + " not found in resource information"); 188 } 189 190 var archInfo = platformInfo.architectures().get(arch); 191 if (archInfo == null) { 192 throw new IOException("Architecture " + arch + " not found for platform " + platform); 193 } 194 195 // Map of <file to extract> to <hash we loaded from the JSON> 196 Map<String, String> filenameToHash = archInfo.fileHashes(); 197 198 var extractionPathString = getExtractionDirectory(); 199 200 if (extractionPathString == null) { 201 // Folder to extract to derived from overall hash 202 String combinedHash = resourceInfo.hash(); 203 204 var defaultExtractionRoot = getDefaultExtractionRoot(); 205 var extractionPath = Paths.get(defaultExtractionRoot, platform, arch, combinedHash); 206 extractionPathString = extractionPath.toString(); 207 208 setExtractionDirectory(extractionPathString); 209 } 210 211 List<String> extractedFiles = new ArrayList<>(); 212 213 for (String file : filenameToHash.keySet()) { 214 try (var stream = clazz.getResourceAsStream(file)) { 215 Objects.requireNonNull(stream); 216 217 var outputFile = Paths.get(extractionPathString, new File(file).getName()); 218 219 String fileHash = filenameToHash.get(file); 220 221 extractedFiles.add(outputFile.toString()); 222 if (outputFile.toFile().exists()) { 223 if (hashEm(outputFile.toFile()).equals(fileHash)) { 224 continue; 225 } else { 226 // Hashes don't match, delete and re-extract 227 System.err.println( 228 outputFile.toAbsolutePath().toString() 229 + " failed validation - deleting and re-extracting"); 230 outputFile.toFile().delete(); 231 } 232 } 233 var parent = outputFile.getParent(); 234 if (parent == null) { 235 throw new IOException("Output file has no parent"); 236 } 237 parent.toFile().mkdirs(); 238 239 try (var os = Files.newOutputStream(outputFile)) { 240 Files.copy(stream, outputFile, StandardCopyOption.REPLACE_EXISTING); 241 } 242 243 if (!hashEm(outputFile.toFile()).equals(fileHash)) { 244 throw new IOException("Hash of extracted file does not match expected hash"); 245 } 246 } 247 } 248 249 return extractedFiles; 250 } 251 252 private static String hashEm(File f) throws IOException { 253 try { 254 MessageDigest fileHash = MessageDigest.getInstance("MD5"); 255 try (var dis = 256 new DigestInputStream(new BufferedInputStream(new FileInputStream(f)), fileHash)) { 257 dis.readAllBytes(); 258 } 259 var ret = HexFormat.of().formatHex(fileHash.digest()); 260 return ret; 261 } catch (NoSuchAlgorithmException e) { 262 throw new IOException("Unable to verify extracted native files", e); 263 } 264 } 265 266 /** 267 * Load a single library from a list of extracted files. 268 * 269 * @param libraryName The library name to load 270 * @param extractedFiles The extracted files to search 271 * @throws IOException If library was not found 272 */ 273 public static void loadLibrary(String libraryName, List<String> extractedFiles) 274 throws IOException { 275 String currentPath = null; 276 try { 277 for (var extractedFile : extractedFiles) { 278 if (extractedFile.contains(libraryName)) { 279 // Load it 280 currentPath = extractedFile; 281 System.load(extractedFile); 282 return; 283 } 284 } 285 throw new IOException("Could not find library " + libraryName); 286 } catch (UnsatisfiedLinkError ule) { 287 throw new IOException(getLoadErrorMessage(currentPath, ule)); 288 } 289 } 290 291 /** 292 * Load a list of native libraries out of a single directory. 293 * 294 * @param <T> The class where the resources would be located 295 * @param clazz The actual class object 296 * @param librariesToLoad List of libraries to load 297 * @throws IOException Throws an IOException if not found 298 */ 299 public static <T> void loadLibraries(Class<T> clazz, String... librariesToLoad) 300 throws IOException { 301 synchronized (extractCompleteLock) { 302 if (extractAndVerifyComplete == false) { 303 // Extract everything 304 filesAlreadyExtracted = extractLibraries(clazz, "/ResourceInformation.json"); 305 extractAndVerifyComplete = true; 306 } 307 308 for (var library : librariesToLoad) { 309 loadLibrary(library, filesAlreadyExtracted); 310 } 311 } 312 } 313}