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 =
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}