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