content: /*
   * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
   * (the "License"). You may not use this work except in compliance with the License, which is
   * available at www.apache.org/licenses/LICENSE-2.0
   *
   * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
   * either express or implied, as more fully set forth in the License.
   *
   * See the NOTICE file distributed with this work for information regarding copyright ownership.
   */
  
  package alluxio.jnifuse.utils;
  
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStream;
  import java.nio.file.Files;
  import java.nio.file.StandardCopyOption;
  import java.util.Optional;
  import java.util.concurrent.atomic.AtomicReference;
  
  /**
   * This class is used to load the shared library from within the jar.
   * The shared library is extracted to a temp folder and loaded from there.
   */
  public class NativeLibraryLoader {
  
    public enum LoadState {
      NOT_LOADED,
      LOADED_2,
      LOADED_3,
    }
  
    private static final Logger LOG = LoggerFactory.getLogger(NativeLibraryLoader.class);
    //singleton
    private static final NativeLibraryLoader INSTANCE = new NativeLibraryLoader();
    private static final AtomicReference<LoadState> LOAD_STATE =
        new AtomicReference<>(LoadState.NOT_LOADED);
  
    private static final String TEMP_FILE_PREFIX = "libjnifuse";
    private static final String TEMP_FILE_SUFFIX = Environment.getJniLibraryExtension();
  
    /**
     * @return the load state
     */
    public static LoadState getLoadState() {
      return LOAD_STATE.get();
    }
  
    private static void setLoadState(LoadState loadState) {
      LOAD_STATE.set(loadState);
    }
  
    /**
     * Get a reference to the NativeLibraryLoader.
     *
     * @return The NativeLibraryLoader
     */
    public static NativeLibraryLoader getInstance() {
      return INSTANCE;
    }
  
    /**
     * Internal interface to write tryLoad in an elegant way.
     *
     * @see Runnable interface cannot have checked exception
     */
    interface Load {
      void load() throws IOException;
    }
  
    /**
     * Try running the loading function.
     *
     * @param load the function to load library
     * @return the error if UnsatisfiedLinkError is encountered, empty if it completes successfully
     * @throws IOException if a filesystem operation fails
     */
    private Optional<UnsatisfiedLinkError> tryLoad(Load load) throws IOException {
      try {
        load.load();
        return Optional.empty();
      } catch (final UnsatisfiedLinkError ule) {
        return Optional.of(ule);
      }
    }
  
    /**
     * Load a version of libjnifuse.
     * <p>
     * Firstly attempts to load the library from <i>java.library.path</i>,
     * if that fails then it falls back to extracting
     * the library from the classpath.
     * {@link NativeLibraryLoader#loadLibraryFromJar(String, String, String)}
     *
     * @return the error why load is failed. Empty if load is successful
     */
    private Optional<UnsatisfiedLinkError> load(
        final String sharedLibraryName, final String jniLibraryName,
        final String sharedLibraryFileName, final String jniLibraryFileName,
        final String tmpDir) throws IOException {
  
      Optional<UnsatisfiedLinkError> err = tryLoad(() -> {
        System.loadLibrary(sharedLibraryName);
        LOG.info("Loaded {} by System.loadLibrary.", sharedLibraryName);
      });
  
      // No error means load is successful.
      // Just return an empty.
      if (!err.isPresent()) {
        return err;
      }
  
      err = tryLoad(() -> {
        System.loadLibrary(jniLibraryName);
        LOG.info("Loaded {} by System.loadLibrary.", jniLibraryName);
      });
  
      if (!err.isPresent()) {
        return err;
      }
  
      return tryLoad(() -> loadLibraryFromJar(sharedLibraryFileName, jniLibraryFileName, tmpDir));
    }
  
    private Optional<UnsatisfiedLinkError> load2(final String tmpDir) throws IOException {
      final String SHARED_LIBRARY_NAME =
          Environment.getSharedLibraryName("jnifuse");
      final String SHARED_LIBRARY_FILE_NAME =
          Environment.getSharedLibraryFileName("jnifuse");
      final String JNI_LIBRARY_NAME =
          Environment.getJniLibraryName("jnifuse");
      final String JNI_LIBRARY_FILE_NAME =
          Environment.getJniLibraryFileName("jnifuse");
  
      Optional<UnsatisfiedLinkError> err = load(
          SHARED_LIBRARY_NAME, JNI_LIBRARY_NAME, SHARED_LIBRARY_FILE_NAME, JNI_LIBRARY_FILE_NAME,
          tmpDir
      );
  
      if (!err.isPresent()) {
        LOG.info("Loaded libjnifuse with libfuse version 2.");
        setLoadState(LoadState.LOADED_2);
      }
  
      return err;
    }
  
    private Optional<UnsatisfiedLinkError> load3(final String tmpDir) throws IOException {
      final String SHARED_LIBRARY_NAME =
          Environment.getSharedLibraryName("jnifuse3");
      final String SHARED_LIBRARY_FILE_NAME =
          Environment.getSharedLibraryFileName("jnifuse3");
      final String JNI_LIBRARY_NAME =
          Environment.getJniLibraryName("jnifuse3");
      final String JNI_LIBRARY_FILE_NAME =
          Environment.getJniLibraryFileName("jnifuse3");
  
      Optional<UnsatisfiedLinkError> err = load(
          SHARED_LIBRARY_NAME, JNI_LIBRARY_NAME, SHARED_LIBRARY_FILE_NAME, JNI_LIBRARY_FILE_NAME,
          tmpDir
      );
  
      if (!err.isPresent()) {
        LOG.info("Loaded libjnifuse with libfuse version 3.");
        setLoadState(LoadState.LOADED_3);
      }
  
      return err;
    }
  
    /**
     * Load the library.
     *
     * @param version     the version of libfuse to load
     * @param tmpDir A temporary directory to use
     *               to copy the native library to when loading from the classpath.
     *               If null, or the empty string, we rely on Java's
     *               {@link File#createTempFile(String, String)}
     *               function to provide a temporary location.
     *               The temporary file will be registered for deletion
     *               on exit.
     * @throws IOException if a filesystem operation fails
     */
    public synchronized void loadLibrary(
        final LibfuseVersion version, final String tmpDir) throws IOException {
  
      Optional<UnsatisfiedLinkError> err;
      switch (version) {
        case VERSION_2:
          err = load2(tmpDir);
          break;
        case VERSION_3:
          err = load3(tmpDir);
          break;
        default:
          // should not fall here
          throw new RuntimeException(String.format("Unsupported libfuse version %d", version));
      }
      if (err.isPresent()) {
        throw err.get();
      }
      return;
    }
  
    /**
     * Attempts to extract the native library
     * from the classpath and load it.
     *
     * @param sharedLibraryFileName The filename of the shared library file
     *                              to be loaded
     * @param jniLibraryFileName    The filename of the jni library
     *                              to be loaded
     * @param tmpDir                A temporary directory to use
     *                              to copy the native library to. If null,
     *                              or the empty string, we rely on Java's
     *                              {@link File#createTempFile(String, String)}
     *                              function to provide a temporary location.
     *                              The temporary file will be registered for deletion
     *                              on exit.
     * @throws IOException if a filesystem operation fails
     */
    void loadLibraryFromJar(final String sharedLibraryFileName,
        final String jniLibraryFileName, final String tmpDir) throws IOException {
      if (LOAD_STATE.get() == LoadState.NOT_LOADED) {
        String libPath = loadLibraryFromJarToTemp(
            sharedLibraryFileName, jniLibraryFileName, tmpDir).getAbsolutePath();
        System.load(libPath);
        LOG.info("Loaded lib by jar from path {}.", libPath);
      }
    }
  
    File loadLibraryFromJarToTemp(final String sharedLibraryFileName,
        final String jniLibraryFileName, final String tmpDir) throws IOException {
      final File temp;
      if (tmpDir == null || tmpDir.isEmpty()) {
        temp = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
      } else {
        temp = new File(tmpDir, jniLibraryFileName);
        if (temp.exists() && !temp.delete()) {
          throw new RuntimeException("File: " + temp.getAbsolutePath()
              + " already exists and cannot be removed.");
        }
        if (!temp.createNewFile()) {
          throw new RuntimeException("File: " + temp.getAbsolutePath()
              + " could not be created.");
        }
      }
  
      if (!temp.exists()) {
        throw new RuntimeException(
            "File " + temp.getAbsolutePath() + " does not exist.");
      } else {
        temp.deleteOnExit();
      }
  
      // attempt to copy the library from the Jar file to the temp destination
      try (final InputStream is = getClass().getClassLoader()
          .getResourceAsStream(sharedLibraryFileName)) {
        if (is == null) {
          throw new RuntimeException(
              sharedLibraryFileName + " was not found inside JAR.");
        } else {
          Files.copy(is, temp.toPath(), StandardCopyOption.REPLACE_EXISTING);
        }
      }
  
      return temp;
    }
  
    /**
     * Private constructor to disallow instantiation.
     */
    private NativeLibraryLoader() {
    }
  }
  