diff --git a/components/native-loader/src/main/java/datadog/nativeloader/ClassLoaderResourcePathLocator.java b/components/native-loader/src/main/java/datadog/nativeloader/ClassLoaderResourcePathLocator.java index 4563facfb81..1aef0ff3ca8 100644 --- a/components/native-loader/src/main/java/datadog/nativeloader/ClassLoaderResourcePathLocator.java +++ b/components/native-loader/src/main/java/datadog/nativeloader/ClassLoaderResourcePathLocator.java @@ -14,8 +14,9 @@ public ClassLoaderResourcePathLocator(final ClassLoader classLoader, final Strin } @Override - public URL locate(String component, String path) { - return this.classLoader.getResource(PathUtils.concatPath(component, this.baseResource, path)); + public URL locate(String optionalComponent, String path) { + return this.classLoader.getResource( + PathUtils.concatPath(optionalComponent, this.baseResource, path)); } @Override diff --git a/components/native-loader/src/main/java/datadog/nativeloader/CompositeLibraryLoadingListener.java b/components/native-loader/src/main/java/datadog/nativeloader/CompositeLibraryLoadingListener.java new file mode 100644 index 00000000000..6f87121b6d2 --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/CompositeLibraryLoadingListener.java @@ -0,0 +1,140 @@ +package datadog.nativeloader; + +import java.net.URL; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +final class CompositeLibraryLoadingListener extends SafeLibraryLoadingListener { + private final Collection listeners; + + CompositeLibraryLoadingListener(LibraryLoadingListener... listeners) { + this(Arrays.asList(listeners)); + } + + CompositeLibraryLoadingListener(Collection listeners) { + this.listeners = listeners; + } + + @Override + public boolean isNop() { + return this.listeners.isEmpty(); + } + + int size() { + return this.listeners.size(); + } + + @Override + public void onResolveDynamic( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + URL optionalUrl) { + for (LibraryLoadingListener listener : this.listeners) { + try { + listener.onResolveDynamic( + platformSpec, optionalComponent, libName, isPreloaded, optionalUrl); + } catch (Throwable ignored) { + } + } + } + + @Override + public void onResolveDynamicFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Throwable optionalCause) { + for (LibraryLoadingListener listener : this.listeners) { + try { + listener.onResolveDynamicFailure(platformSpec, optionalComponent, libName, optionalCause); + } catch (Throwable ignored) { + } + } + } + + @Override + public void onLoad( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + Path optionalLibPath) { + for (LibraryLoadingListener listener : this.listeners) { + try { + listener.onLoad(platformSpec, optionalComponent, libName, isPreloaded, optionalLibPath); + } catch (Throwable ignored) { + } + } + } + + @Override + public void onLoadFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Throwable optionalCause) { + for (LibraryLoadingListener listener : this.listeners) { + try { + listener.onLoadFailure(platformSpec, optionalComponent, libName, optionalCause); + } catch (Throwable ignored) { + } + } + } + + @Override + public void onTempFileCreated( + PlatformSpec platformSpec, String optionalComponent, String libName, Path tempFile) { + for (LibraryLoadingListener listener : this.listeners) { + try { + listener.onTempFileCreated(platformSpec, optionalComponent, libName, tempFile); + } catch (Throwable ignored) { + } + } + } + + @Override + public void onTempFileCreationFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Path tempDir, + String libExt, + Throwable optionalCause) { + for (LibraryLoadingListener listener : this.listeners) { + try { + listener.onTempFileCreationFailure( + platformSpec, optionalComponent, libName, tempDir, libExt, optionalCause); + } catch (Throwable ignored) { + } + } + } + + @Override + public void onTempFileCleanup( + PlatformSpec platformSpec, String optionalComponent, String libName, Path tempPath) { + for (LibraryLoadingListener listener : this.listeners) { + try { + listener.onTempFileCleanup(platformSpec, optionalComponent, libName, tempPath); + } catch (Throwable ignored) { + } + } + } + + @Override + public CompositeLibraryLoadingListener join(LibraryLoadingListener... listeners) { + ArrayList combinedListeners = + new ArrayList<>(this.listeners.size() + listeners.length); + combinedListeners.addAll(this.listeners); + combinedListeners.addAll(Arrays.asList(listeners)); + return new CompositeLibraryLoadingListener(combinedListeners); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + ":" + this.listeners.toString(); + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/FlatDirLibraryResolver.java b/components/native-loader/src/main/java/datadog/nativeloader/FlatDirLibraryResolver.java index 29b8f5e16c5..9730b8de7f5 100644 --- a/components/native-loader/src/main/java/datadog/nativeloader/FlatDirLibraryResolver.java +++ b/components/native-loader/src/main/java/datadog/nativeloader/FlatDirLibraryResolver.java @@ -13,7 +13,7 @@ private FlatDirLibraryResolver() {} @Override public final URL resolve( - PathLocator pathLocator, String component, PlatformSpec platformSpec, String libName) + PathLocator pathLocator, PlatformSpec platformSpec, String optionalComponent, String libName) throws Exception { PathLocatorHelper pathLocatorHelper = new PathLocatorHelper(libName, pathLocator); @@ -28,22 +28,22 @@ public final URL resolve( if (libcPath != null) { String specializedPath = regularPath + "-" + libcPath; - url = pathLocatorHelper.locate(component, specializedPath + "/" + libFileName); + url = pathLocatorHelper.locate(optionalComponent, specializedPath + "/" + libFileName); if (url != null) return url; } - url = pathLocatorHelper.locate(component, regularPath + "/" + libFileName); + url = pathLocatorHelper.locate(optionalComponent, regularPath + "/" + libFileName); if (url != null) return url; - url = pathLocatorHelper.locate(component, osPath + "/" + libFileName); + url = pathLocatorHelper.locate(optionalComponent, osPath + "/" + libFileName); if (url != null) return url; // fallback to searching at top-level, mostly concession to good out-of-box behavior // with java.library.path - url = pathLocatorHelper.locate(component, libFileName); + url = pathLocatorHelper.locate(optionalComponent, libFileName); if (url != null) return url; - if (component != null) { + if (optionalComponent != null) { url = pathLocatorHelper.locate(null, libFileName); if (url != null) return url; } diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibDirBasedPathLocator.java b/components/native-loader/src/main/java/datadog/nativeloader/LibDirBasedPathLocator.java index 5199ae0fc3a..2ed0528c161 100644 --- a/components/native-loader/src/main/java/datadog/nativeloader/LibDirBasedPathLocator.java +++ b/components/native-loader/src/main/java/datadog/nativeloader/LibDirBasedPathLocator.java @@ -14,8 +14,8 @@ public LibDirBasedPathLocator(File... libDirs) { } @Override - public URL locate(String component, String path) { - String fullPath = PathUtils.concatPath(component, path); + public URL locate(String optionalComponent, String path) { + String fullPath = PathUtils.concatPath(optionalComponent, path); for (File libDir : this.libDirs) { File libFile = new File(libDir, fullPath); diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibFile.java b/components/native-loader/src/main/java/datadog/nativeloader/LibFile.java index fb2e27d61d4..80a1957e5e8 100644 --- a/components/native-loader/src/main/java/datadog/nativeloader/LibFile.java +++ b/components/native-loader/src/main/java/datadog/nativeloader/LibFile.java @@ -16,66 +16,114 @@ public final class LibFile implements AutoCloseable { static final boolean NO_CLEAN_UP = false; static final boolean CLEAN_UP = true; - static final LibFile preloaded(String libName) { - return new LibFile(libName, null, NO_CLEAN_UP); + static final LibFile preloaded( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + SafeLibraryLoadingListener listeners) { + return new LibFile(platformSpec, optionalComponent, libName, null, NO_CLEAN_UP, listeners); } - static final LibFile fromFile(String libName, File file) { - return new LibFile(libName, file, NO_CLEAN_UP); + static final LibFile fromFile( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + File optionalFile, + SafeLibraryLoadingListener listeners) { + return new LibFile( + platformSpec, optionalComponent, libName, optionalFile, NO_CLEAN_UP, listeners); } - static final LibFile fromTempFile(String libName, File file) { - return new LibFile(libName, file, CLEAN_UP); + static final LibFile fromTempFile( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + File optionalFile, + SafeLibraryLoadingListener listeners) { + return new LibFile(platformSpec, optionalComponent, libName, optionalFile, CLEAN_UP, listeners); } + final PlatformSpec platformSpec; + final String optionalComponent; final String libName; - final File file; + final File optionalFile; final boolean needsCleanup; - LibFile(String libName, File file, boolean needsCleanup) { + final SafeLibraryLoadingListener listeners; + + LibFile( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + File optionalFile, + boolean needsCleanup, + SafeLibraryLoadingListener listeners) { + this.platformSpec = platformSpec; + this.optionalComponent = optionalComponent; this.libName = libName; - this.file = file; + this.optionalFile = optionalFile; this.needsCleanup = needsCleanup; + + this.listeners = listeners; } /** Indicates if this library was "preloaded" */ public boolean isPreloaded() { - return (this.file == null); + return (this.optionalFile == null); } /** Loads the underlying library into the JVM */ public void load() throws LibraryLoadException { - if (this.isPreloaded()) return; + boolean isPreloaded = this.isPreloaded(); + if (isPreloaded) { + this.listeners.onLoad( + this.platformSpec, this.optionalComponent, this.libName, isPreloaded, null); + return; + } try { Runtime.getRuntime().load(this.getAbsolutePath()); + + if (true) throw new RuntimeException("real load - worked?"); } catch (Throwable t) { + this.listeners.onLoadFailure(this.platformSpec, this.optionalComponent, this.libName, t); throw new LibraryLoadException(this.libName, t); } + + this.listeners.onLoad( + this.platformSpec, + this.optionalComponent, + this.libName, + isPreloaded, + this.optionalFile.toPath()); } /** Provides a File to the library -- returns null for pre-loaded libraries */ public final File toFile() { - return this.file; + return this.optionalFile; } /** Provides a Path to the library -- return null for pre-loaded libraries */ public final Path toPath() { - return this.file == null ? null : this.file.toPath(); + return this.optionalFile == null ? null : this.optionalFile.toPath(); } /** Provides the an absolute path to the library -- returns null for pre-loaded libraries */ public final String getAbsolutePath() { - return this.file == null ? null : this.file.getAbsolutePath(); + return this.optionalFile == null ? null : this.optionalFile.getAbsolutePath(); } - /** Schedules clean-up of underlying file -- if the file is a temp file */ + /** Schedules clean-up of underlying optionalFile -- if the file is a temp file */ @Override public void close() { if (this.needsCleanup) { - NativeLoader.delete(this.file); + boolean deleted = NativeLoader.delete(this.optionalFile); + if (deleted) { + this.listeners.onTempFileCleanup( + this.platformSpec, this.optionalComponent, this.libName, this.optionalFile.toPath()); + } } } } diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibraryLoadingListener.java b/components/native-loader/src/main/java/datadog/nativeloader/LibraryLoadingListener.java new file mode 100644 index 00000000000..8fa68a4feb0 --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/LibraryLoadingListener.java @@ -0,0 +1,78 @@ +package datadog.nativeloader; + +import java.net.URL; +import java.nio.file.Path; + +public interface LibraryLoadingListener { + /** + * Called when a dynamic library is resolved. This includes resolving a pre-loaded or already + * loaded library + * + *

If the library is pre-loaded optionalUrl will be null + */ + default void onResolveDynamic( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + URL optionalUrl) {} + + /** + * Called when a dynamic library fails to resolve. This can occur because the library was not + * found -- or an exception occurred during resolution + */ + default void onResolveDynamicFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Throwable optionalCause) {} + + /** + * Called when a dynamic library loads successfully This includes loading a pre-loaded or already + * loaded library + */ + default void onLoad( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + Path optionalLibPath) {} + + /** Called when a dynamic library fails to load */ + default void onLoadFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Throwable optionalCause) {} + + /** Called when a temp file is successfully created to hold the library */ + default void onTempFileCreated( + PlatformSpec platformSpec, String optionalComponent, String libName, Path tempFile) {} + + /** Called when a temp file could not be created */ + default void onTempFileCreationFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Path tempDir, + String libExt, + Throwable optionalCause) {} + + /** Called when a temp file is cleaned up */ + default void onTempFileCleanup( + PlatformSpec platformSpec, String optionalComponent, String libName, Path tempFile) {} +} + +/** + * "safe" listeners are used inside NativeLoader to avoid exceptions leaking out + * + *

The "safe" listeners are {@link CompositeLibraryLoadingListener} used to wrap regular + * listeners and {@link NopLibraryLoadingListener} used to optimize the nop case. + */ +abstract class SafeLibraryLoadingListener implements LibraryLoadingListener { + /** Used to create a new safe listener with the provided listeners append onto this one */ + public abstract SafeLibraryLoadingListener join(LibraryLoadingListener... listeners); + + /** Indicates if all listener operates are nops */ + public abstract boolean isNop(); +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolver.java b/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolver.java index 4d28aae2794..66ab56c0169 100644 --- a/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolver.java +++ b/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolver.java @@ -15,6 +15,7 @@ default boolean isPreloaded(PlatformSpec platform, String libName) { return false; } - URL resolve(PathLocator pathLocator, String component, PlatformSpec platformSpec, String libName) + URL resolve( + PathLocator pathLocator, PlatformSpec platformSpec, String optionalComponent, String libName) throws Exception; } diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolvers.java b/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolvers.java index 6ef8cec3c4b..bcbb434a3a1 100644 --- a/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolvers.java +++ b/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolvers.java @@ -27,9 +27,12 @@ public boolean isPreloaded(PlatformSpec platform, String libName) { @Override public URL resolve( - PathLocator pathLocator, String component, PlatformSpec platformSpec, String libName) + PathLocator pathLocator, + PlatformSpec platformSpec, + String optionalComponent, + String libName) throws Exception { - return baseResolver.resolve(pathLocator, component, platformSpec, libName); + return baseResolver.resolve(pathLocator, platformSpec, optionalComponent, libName); } }; } diff --git a/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java b/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java index 17af0b6f7ab..8a55eabe570 100644 --- a/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java +++ b/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java @@ -11,10 +11,13 @@ import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Set; /** - * NativeLoader is intended as more feature rich replacement for calling {@link + * NativeLoader is intended as a more feature rich replacement for calling {@link * System#loadLibrary(String)} directly. NativeLoader can be used to find the corresponding platform * specific library using pluggable strategies -- for both path determination {@link * LibraryResolver} and path resolution {@link PathLocator} @@ -26,6 +29,7 @@ public static final class Builder { private String[] preloadedLibNames; private LibraryResolver libResolver; private PathLocator pathLocator; + private List listeners = new ArrayList<>(); Builder() {} @@ -138,6 +142,16 @@ public Builder tempDir(String tmpPath) { return this.tempDir(Paths.get(tmpPath)); } + public Builder addListener(LibraryLoadingListener listener) { + this.listeners.add(listener); + return this; + } + + public Builder addListeners(LibraryLoadingListener... listeners) { + this.listeners.addAll(Arrays.asList(listeners)); + return this; + } + /** Constructs and returns the {@link NativeLoader} */ public NativeLoader build() { return new NativeLoader(this); @@ -151,6 +165,12 @@ PathLocator pathLocator() { return (this.pathLocator == null) ? PathLocators.defaultPathLocator() : this.pathLocator; } + SafeLibraryLoadingListener listeners() { + return this.listeners.isEmpty() + ? NopLibraryLoadingListener.INSTANCE + : new CompositeLibraryLoadingListener(this.listeners); + } + LibraryResolver libResolver() { LibraryResolver baseResolver = (this.libResolver == null) ? LibraryResolvers.defaultLibraryResolver() : this.libResolver; @@ -169,18 +189,30 @@ public static final Builder builder() { return new Builder(); } + private static final URL NO_URL = null; + private static final Throwable NO_CAUSE = null; + private static final LibraryLoadingListener[] EMPTY_LISTENERS = {}; + private final PlatformSpec defaultPlatformSpec; private final LibraryResolver libResolver; private final PathLocator pathResolver; + private final SafeLibraryLoadingListener listeners; private final Path tempDir; private NativeLoader(Builder builder) { this.defaultPlatformSpec = builder.platformSpec(); this.libResolver = builder.libResolver(); this.pathResolver = builder.pathLocator(); + this.listeners = builder.listeners(); this.tempDir = builder.tempDir(); } + public boolean isPlatformSupported() { + if (this.defaultPlatformSpec.isUnknownOs()) return false; + if (this.defaultPlatformSpec.isUnknownArch()) return false; + return true; + } + /** Indicates if a library is considered "pre-loaded" */ public boolean isPreloaded(String libName) { return this.libResolver.isPreloaded(this.defaultPlatformSpec, libName); @@ -193,24 +225,47 @@ public boolean isPreloaded(PlatformSpec platformSpec, String libName) { /** Loads a library */ public void load(String libName) throws LibraryLoadException { - this.load(null, libName); + this.loadImpl(null, libName, EMPTY_LISTENERS); + } + + public void load(String libName, LibraryLoadingListener... scopedListeners) + throws LibraryLoadException { + this.loadImpl(null, libName, scopedListeners); } /** Loads a library associated with an associated component */ public void load(String component, String libName) throws LibraryLoadException { - try (LibFile libFile = this.resolveDynamic(component, libName)) { + this.loadImpl(component, libName, EMPTY_LISTENERS); + } + + public void load(String component, String libName, LibraryLoadingListener... scopedListeners) + throws LibraryLoadException { + this.loadImpl(component, libName, scopedListeners); + } + + private void loadImpl(String component, String libName, LibraryLoadingListener... scopedListeners) + throws LibraryLoadException { + + // scopedListeners are attached to the LibFile by resolveDynamicImpl + try (LibFile libFile = + this.resolveDynamicImpl(this.defaultPlatformSpec, component, libName, scopedListeners)) { libFile.load(); } } /** Resolves a library to a LibFile - creating a temporary file if necessary */ public LibFile resolveDynamic(String libName) throws LibraryLoadException { - return this.resolveDynamic((String) null, libName); + return this.resolveDynamicImpl(this.defaultPlatformSpec, null, libName, EMPTY_LISTENERS); + } + + public LibFile resolveDynamic(String libName, LibraryLoadingListener... scopedListeners) + throws LibraryLoadException { + return this.resolveDynamicImpl(this.defaultPlatformSpec, null, libName, scopedListeners); } /** Resolves a library with an associated component */ public LibFile resolveDynamic(String component, String libName) throws LibraryLoadException { - return this.resolveDynamic(component, this.defaultPlatformSpec, libName); + return this.resolveDynamicImpl(this.defaultPlatformSpec, component, libName, EMPTY_LISTENERS); } /** @@ -219,62 +274,104 @@ public LibFile resolveDynamic(String component, String libName) throws LibraryLo */ public LibFile resolveDynamic(PlatformSpec platformSpec, String libName) throws LibraryLoadException { - return this.resolveDynamic(null, platformSpec, libName); + return this.resolveDynamicImpl(platformSpec, null, libName, EMPTY_LISTENERS); + } + + public LibFile resolveDynamic( + PlatformSpec platformSpec, String libName, LibraryLoadingListener... scopedListeners) + throws LibraryLoadException { + return this.resolveDynamicImpl(platformSpec, null, libName, scopedListeners); } /** * Resolves a library with an associated component with a different {@link PlatformSpec} than the * default */ - public LibFile resolveDynamic(String component, PlatformSpec platformSpec, String libName) + public LibFile resolveDynamic(PlatformSpec platformSpec, String component, String libName) throws LibraryLoadException { + return this.resolveDynamicImpl(platformSpec, component, libName, EMPTY_LISTENERS); + } + + private LibFile resolveDynamicImpl( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + LibraryLoadingListener[] scopedListeners) + throws LibraryLoadException { + SafeLibraryLoadingListener allListeners = + (scopedListeners == null + || scopedListeners == EMPTY_LISTENERS + || scopedListeners.length == 0) + ? this.listeners + : this.listeners.join(scopedListeners); + if (platformSpec.isUnknownOs() || platformSpec.isUnknownArch()) { + allListeners.onResolveDynamicFailure(platformSpec, optionalComponent, libName, NO_CAUSE); throw new LibraryLoadException(libName, "Unsupported platform"); } - if (this.isPreloaded(platformSpec, libName)) { - return LibFile.preloaded(libName); + boolean isPreloaded = this.isPreloaded(platformSpec, libName); + if (isPreloaded) { + allListeners.onResolveDynamic(platformSpec, optionalComponent, libName, isPreloaded, NO_URL); + return LibFile.preloaded(platformSpec, optionalComponent, libName, allListeners); } URL url; try { - url = this.libResolver.resolve(this.pathResolver, component, platformSpec, libName); + url = this.libResolver.resolve(this.pathResolver, platformSpec, optionalComponent, libName); } catch (LibraryLoadException e) { // don't wrap if it is already a LibraryLoadException + allListeners.onResolveDynamicFailure(platformSpec, optionalComponent, libName, e.getCause()); throw e; } catch (Throwable t) { + allListeners.onResolveDynamicFailure(platformSpec, optionalComponent, libName, t); throw new LibraryLoadException(libName, t); } if (url == null) { + allListeners.onResolveDynamicFailure(platformSpec, optionalComponent, libName, NO_CAUSE); throw new LibraryLoadException(libName); } - return toLibFile(platformSpec, libName, url); - } - private LibFile toLibFile(PlatformSpec platformSpec, String libName, URL url) - throws LibraryLoadException { + // For listener purposes - at this point resolution completed successfully + // Although, the resolveDynamic method can still fail if we need a temp file and cannot create + // it + allListeners.onResolveDynamic(platformSpec, optionalComponent, libName, isPreloaded, url); + if (url.getProtocol().equals("file")) { - return LibFile.fromFile(libName, new File(url.getPath())); + return LibFile.fromFile( + platformSpec, optionalComponent, libName, new File(url.getPath()), allListeners); } else { - String libExt = PathUtils.dynamicLibExtension(platformSpec); - + Path tempFile; try { - Path tempFile = TempFileHelper.createTempFile(this.tempDir, libName, libExt); + tempFile = createTempFile(this.tempDir, platformSpec, libName, url); + allListeners.onTempFileCreated(platformSpec, optionalComponent, libName, tempFile); - try (InputStream in = url.openStream()) { - Files.copy(in, tempFile, StandardCopyOption.REPLACE_EXISTING); - } - - return LibFile.fromTempFile(libName, tempFile.toFile()); + return LibFile.fromTempFile( + platformSpec, optionalComponent, libName, tempFile.toFile(), allListeners); } catch (Throwable t) { + allListeners.onTempFileCreationFailure( + platformSpec, optionalComponent, libName, this.tempDir, libName, t); + throw new LibraryLoadException(libName, t); } } } - static void delete(File tempFile) { - TempFileHelper.delete(tempFile); + private static Path createTempFile( + Path tempDir, PlatformSpec platformSpec, String libName, URL url) throws IOException { + String libExt = PathUtils.dynamicLibExtension(platformSpec); + + Path tempFile = TempFileHelper.createTempFile(tempDir, libName, libExt); + try (InputStream in = url.openStream()) { + Files.copy(in, tempFile, StandardCopyOption.REPLACE_EXISTING); + } + + return tempFile; + } + + static boolean delete(File tempFile) { + return TempFileHelper.delete(tempFile); } static final class TempFileHelper { @@ -296,9 +393,10 @@ static Path createTempFile(Path tempDir, String libname, String libExt) } } - static void delete(File tempFile) { + static boolean delete(File tempFile) { boolean deleted = tempFile.delete(); if (!deleted) tempFile.deleteOnExit(); + return deleted; } } } diff --git a/components/native-loader/src/main/java/datadog/nativeloader/NestedDirLibraryResolver.java b/components/native-loader/src/main/java/datadog/nativeloader/NestedDirLibraryResolver.java index 966fd2c03e8..0137ca1d07e 100644 --- a/components/native-loader/src/main/java/datadog/nativeloader/NestedDirLibraryResolver.java +++ b/components/native-loader/src/main/java/datadog/nativeloader/NestedDirLibraryResolver.java @@ -11,7 +11,7 @@ final class NestedDirLibraryResolver implements LibraryResolver { @Override public final URL resolve( - PathLocator pathLocator, String component, PlatformSpec platformSpec, String libName) + PathLocator pathLocator, PlatformSpec platformSpec, String optionalComponent, String libName) throws Exception { PathLocatorHelper pathLocatorHelper = new PathLocatorHelper(libName, pathLocator); @@ -27,22 +27,22 @@ public final URL resolve( if (libcPath != null) { String specializedPath = regularPath + "/" + libcPath; - url = pathLocatorHelper.locate(component, specializedPath + "/" + libFileName); + url = pathLocatorHelper.locate(optionalComponent, specializedPath + "/" + libFileName); if (url != null) return url; } - url = pathLocatorHelper.locate(component, regularPath + "/" + libFileName); + url = pathLocatorHelper.locate(optionalComponent, regularPath + "/" + libFileName); if (url != null) return url; - url = pathLocatorHelper.locate(component, osPath + "/" + libFileName); + url = pathLocatorHelper.locate(optionalComponent, osPath + "/" + libFileName); if (url != null) return url; // fallback to searching at top-level, mostly concession to good out-of-box behavior // with java.library.path - url = pathLocatorHelper.locate(component, libFileName); + url = pathLocatorHelper.locate(optionalComponent, libFileName); if (url != null) return url; - if (component != null) { + if (optionalComponent != null) { url = pathLocatorHelper.locate(null, libFileName); if (url != null) return url; } diff --git a/components/native-loader/src/main/java/datadog/nativeloader/NopLibraryLoadingListener.java b/components/native-loader/src/main/java/datadog/nativeloader/NopLibraryLoadingListener.java new file mode 100644 index 00000000000..afec8dc1b1f --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/NopLibraryLoadingListener.java @@ -0,0 +1,19 @@ +package datadog.nativeloader; + +import java.util.Arrays; + +final class NopLibraryLoadingListener extends SafeLibraryLoadingListener { + static final NopLibraryLoadingListener INSTANCE = new NopLibraryLoadingListener(); + + private NopLibraryLoadingListener() {} + + @Override + public boolean isNop() { + return true; + } + + @Override + public SafeLibraryLoadingListener join(LibraryLoadingListener... listeners) { + return new CompositeLibraryLoadingListener(Arrays.asList(listeners)); + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/PathLocator.java b/components/native-loader/src/main/java/datadog/nativeloader/PathLocator.java index e7cc2e14f20..aae3c2e0b5a 100644 --- a/components/native-loader/src/main/java/datadog/nativeloader/PathLocator.java +++ b/components/native-loader/src/main/java/datadog/nativeloader/PathLocator.java @@ -14,5 +14,5 @@ public interface PathLocator { *

If the returned URL uses a non-file protocol, then {@link NativeLoader} will call {@link * URL#openStream()} and copy the contents to a temporary file */ - URL locate(String component, String path) throws Exception; + URL locate(String optionalComponent, String path) throws Exception; } diff --git a/components/native-loader/src/main/java/datadog/nativeloader/PathLocatorHelper.java b/components/native-loader/src/main/java/datadog/nativeloader/PathLocatorHelper.java index 38c976c5c86..8842d642afb 100644 --- a/components/native-loader/src/main/java/datadog/nativeloader/PathLocatorHelper.java +++ b/components/native-loader/src/main/java/datadog/nativeloader/PathLocatorHelper.java @@ -18,9 +18,9 @@ public PathLocatorHelper(String libName, PathLocator locator) { } @Override - public URL locate(String component, String path) { + public URL locate(String optionalComponent, String path) { try { - return this.locator.locate(component, path); + return this.locator.locate(optionalComponent, path); } catch (Throwable t) { if (this.firstCause == null) this.firstCause = t; return null; diff --git a/components/native-loader/src/test/java/datadog/nativeloader/CapturingPathLocator.java b/components/native-loader/src/test/java/datadog/nativeloader/CapturingPathLocator.java index 257e5ba9958..b002b2c144d 100644 --- a/components/native-loader/src/test/java/datadog/nativeloader/CapturingPathLocator.java +++ b/components/native-loader/src/test/java/datadog/nativeloader/CapturingPathLocator.java @@ -32,7 +32,7 @@ public static final void test( String comp = "comp"; CapturingPathLocator fullCaptureLocator = new CapturingPathLocator(Integer.MAX_VALUE); - resolver.resolve(fullCaptureLocator, comp, platformSpec, "test"); + resolver.resolve(fullCaptureLocator, platformSpec, comp, "test"); for (int i = 0; !fullCaptureLocator.isEmpty(); ++i) { if (i >= expectedPaths.length) { @@ -52,7 +52,7 @@ public static final void test( for (int i = 0; i < expectedPaths.length; ++i) { CapturingPathLocator fallbackLocator = new CapturingPathLocator(i); - resolver.resolve(fallbackLocator, comp, platformSpec, "test"); + resolver.resolve(fallbackLocator, platformSpec, comp, "test"); for (int j = 0; j <= i; ++j) { fallbackLocator.assertRequested(comp, expectedPaths[j]); @@ -62,7 +62,7 @@ public static final void test( if (withSkipCompFallback) { CapturingPathLocator fallbackLocator = new CapturingPathLocator(expectedPaths.length); - resolver.resolve(fallbackLocator, comp, platformSpec, "test"); + resolver.resolve(fallbackLocator, platformSpec, comp, "test"); for (int j = 0; j < expectedPaths.length; ++j) { fallbackLocator.assertRequested(comp, expectedPaths[j]); @@ -87,8 +87,8 @@ public CapturingPathLocator(int simulateNotFoundCount) { } @Override - public URL locate(String component, String path) { - this.locateRequests.addLast(new LocateRequest(component, path)); + public URL locate(String optionalComponent, String path) { + this.locateRequests.addLast(new LocateRequest(optionalComponent, path)); if (this.numRequests++ < this.simulateNotFoundCount) return null; try { diff --git a/components/native-loader/src/test/java/datadog/nativeloader/CompositeLibraryLoadingListenerTest.java b/components/native-loader/src/test/java/datadog/nativeloader/CompositeLibraryLoadingListenerTest.java new file mode 100644 index 00000000000..019f09c2138 --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/CompositeLibraryLoadingListenerTest.java @@ -0,0 +1,176 @@ +package datadog.nativeloader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +public final class CompositeLibraryLoadingListenerTest { + @Test + public void onResolveDynamic() throws MalformedURLException { + TestLibraryLoadingListener listener1 = + new TestLibraryLoadingListener().expectResolveDynamic("foo"); + + TestLibraryLoadingListener listener2 = listener1.copy(); + + listeners(listener1, listener2) + .onResolveDynamic( + PlatformSpec.defaultPlatformSpec(), null, "foo", false, new URL("http://localhost")); + + listener1.assertDone(); + listener2.assertDone(); + } + + @Test + public void onResolveDynamicFailure() { + TestLibraryLoadingListener listener1 = + new TestLibraryLoadingListener().expectResolveDynamicFailure("foo"); + + TestLibraryLoadingListener listener2 = listener1.copy(); + + listeners(listener1, listener2) + .onResolveDynamicFailure( + PlatformSpec.defaultPlatformSpec(), null, "foo", new Exception("foo")); + + listener1.assertDone(); + listener2.assertDone(); + } + + @Test + public void onLoad() { + TestLibraryLoadingListener listener1 = new TestLibraryLoadingListener().expectLoad("foo"); + + TestLibraryLoadingListener listener2 = listener1.copy(); + + listeners(listener1, listener2) + .onLoad(PlatformSpec.defaultPlatformSpec(), null, "foo", false, null); + + listener1.assertDone(); + listener2.assertDone(); + } + + @Test + public void onLoadFailure() { + TestLibraryLoadingListener listener1 = + new TestLibraryLoadingListener().expectLoadFailure("foo"); + + TestLibraryLoadingListener listener2 = listener1.copy(); + + listeners(listener1, listener2) + .onLoadFailure(PlatformSpec.defaultPlatformSpec(), null, "foo", null); + + listener1.assertDone(); + listener2.assertDone(); + } + + @Test + public void onTempFileCreated() { + TestLibraryLoadingListener listener1 = + new TestLibraryLoadingListener().expectTempFileCreated("foo"); + + TestLibraryLoadingListener listener2 = listener1.copy(); + + listeners(listener1, listener2) + .onTempFileCreated( + PlatformSpec.defaultPlatformSpec(), null, "foo", Paths.get("/tmp/foo.dll")); + + listener1.assertDone(); + listener2.assertDone(); + } + + @Test + public void onTempFileCreationFailure() { + TestLibraryLoadingListener listener1 = + new TestLibraryLoadingListener().expectTempFileCreationFailure("foo"); + + TestLibraryLoadingListener listener2 = listener1.copy(); + + listeners(listener1, listener2) + .onTempFileCreationFailure( + PlatformSpec.defaultPlatformSpec(), + null, + "foo", + Paths.get("/tmp"), + "dylib", + new IOException("perm")); + + listener1.assertDone(); + listener2.assertDone(); + } + + @Test + public void onTempFileCleanup() { + TestLibraryLoadingListener listener1 = + new TestLibraryLoadingListener().expectTempFileCleanup("foo"); + + TestLibraryLoadingListener listener2 = listener1.copy(); + + listeners(listener1, listener2) + .onTempFileCleanup( + PlatformSpec.defaultPlatformSpec(), null, "foo", Paths.get("/tmp/foo.dll")); + + listener1.assertDone(); + listener2.assertDone(); + } + + @Test + public void join() { + TestLibraryLoadingListener listener1 = new TestLibraryLoadingListener().expectLoad("foo"); + + CompositeLibraryLoadingListener composite = new CompositeLibraryLoadingListener(listener1); + assertEquals(1, composite.size()); + + TestLibraryLoadingListener listener2 = listener1.copy(); + + CompositeLibraryLoadingListener composite2 = composite.join(listener2); + assertEquals(2, composite2.size()); + + TestLibraryLoadingListener listener3 = listener1.copy(); + TestLibraryLoadingListener listener4 = listener1.copy(); + + CompositeLibraryLoadingListener finalComposite = composite2.join(listener3, listener4); + assertEquals(4, finalComposite.size()); + + finalComposite.onLoad( + PlatformSpec.defaultPlatformSpec(), null, "foo", false, Paths.get("/tmp/foo.dll")); + + listener1.assertDone(); + listener2.assertDone(); + listener3.assertDone(); + listener4.assertDone(); + } + + @Test + public void _toString() { + // just for coverage + assertNotNull(new CompositeLibraryLoadingListener().toString()); + } + + /* + * Constructs a composite listener that includes the provided listeners + * To test robustness... + * - adds in additional failing listeners + * - adds in additional default listeners + * - shuffles the order of the listeners + */ + static CompositeLibraryLoadingListener listeners(LibraryLoadingListener... listeners) { + List shuffledListeners = new ArrayList<>(listeners.length * 3); + shuffledListeners.addAll(Arrays.asList(listeners)); + + for (int i = 0; i < listeners.length; ++i) { + shuffledListeners.add(new LibraryLoadingListener() {}); + shuffledListeners.add(new ThrowingLibraryLoadingListener()); + } + + Collections.shuffle(shuffledListeners); + return new CompositeLibraryLoadingListener(shuffledListeners); + } +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderBuilderTest.java b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderBuilderTest.java index db04ea859e1..db5a25b4541 100644 --- a/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderBuilderTest.java +++ b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderBuilderTest.java @@ -26,6 +26,46 @@ public void customPlatformSpec() { assertSame(platformSpec, builder.platformSpec()); } + @Test + public void defaultListeners() { + NativeLoader.Builder builder = NativeLoader.builder(); + + assertTrue(builder.listeners().isNop()); + } + + @Test + public void addListener() { + TestLibraryLoadingListener listener1 = new TestLibraryLoadingListener().expectLoad("foo"); + TestLibraryLoadingListener listener2 = listener1.copy(); + + NativeLoader.Builder builder = + NativeLoader.builder().addListener(listener1).addListener(listener2); + + SafeLibraryLoadingListener listener = builder.listeners(); + assertFalse(listener.isNop()); + + listener.onLoad(builder.platformSpec(), null, "foo", false, Paths.get("/tmp/foo.dylib")); + + listener1.assertDone(); + listener2.assertDone(); + } + + @Test + public void addListeners() { + TestLibraryLoadingListener listener1 = new TestLibraryLoadingListener().expectLoad("foo"); + TestLibraryLoadingListener listener2 = listener1.copy(); + + NativeLoader.Builder builder = NativeLoader.builder().addListeners(listener1, listener2); + + SafeLibraryLoadingListener listener = builder.listeners(); + assertFalse(listener.isNop()); + + listener.onLoad(builder.platformSpec(), null, "foo", false, Paths.get("/tmp/foo.dylib")); + + listener1.assertDone(); + listener2.assertDone(); + } + @Test public void defaultLibraryResolver() { NativeLoader.Builder builder = NativeLoader.builder(); diff --git a/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java index a122e88bea4..0a44b91f912 100644 --- a/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java +++ b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.io.File; import java.io.IOException; @@ -30,14 +31,14 @@ public class NativeLoaderTest { @Test public void preloaded() throws LibraryLoadException { - NativeLoader loader = NativeLoader.builder().preloaded("dne1", "dne2").build(); + NativeLoader loader = NativeLoader.builder().preloaded("preloaded1", "preloaded2").build(); - assertTrue(loader.isPreloaded("dne1")); - assertTrue(loader.isPreloaded("dne2")); + assertTrue(loader.isPreloaded("preloaded1")); + assertTrue(loader.isPreloaded("preloaded2")); - assertFalse(loader.isPreloaded("dne3")); + assertFalse(loader.isPreloaded("dne")); - try (LibFile lib = loader.resolveDynamic("dne1")) { + try (LibFile lib = loader.resolveDynamic("preloaded1")) { assertPreloaded(lib); // already considered loaded -- so this is a nop @@ -45,18 +46,83 @@ public void preloaded() throws LibraryLoadException { } // already considered loaded -- so this is a nop - loader.load("dne2"); + loader.load("preloaded2"); // not already loaded - so passes through to underlying resolver - assertThrows(LibraryLoadException.class, () -> loader.load("dne3")); + assertThrows(LibraryLoadException.class, () -> loader.load("dne")); + } + + @Test + public void preloaded_listenerSupport() throws LibraryLoadException { + TestLibraryLoadingListener sharedListener = new TestLibraryLoadingListener(); + + NativeLoader loader = + NativeLoader.builder() + .preloaded("preloaded1", "preloaded2") + .addListener(sharedListener) + .build(); + + // debatable - but no listener calls just for checking + assertTrue(loader.isPreloaded("preloaded1")); + assertTrue(loader.isPreloaded("preloaded2")); + + sharedListener.expectResolvePreloaded("preloaded1"); + sharedListener.expectLoadPreloaded("preloaded1"); + + TestLibraryLoadingListener scopedListener1 = + new TestLibraryLoadingListener() + .expectResolvePreloaded("preloaded1") + .expectLoadPreloaded("preloaded1"); + + try (LibFile lib = loader.resolveDynamic("preloaded1", scopedListener1)) { + lib.load(); + } + + sharedListener.assertDone(); + scopedListener1.assertDone(); + + sharedListener.expectResolvePreloaded("preloaded2"); + sharedListener.expectLoadPreloaded("preloaded2"); + + TestLibraryLoadingListener scopedListener2 = + new TestLibraryLoadingListener() + .expectResolvePreloaded("preloaded2") + .expectLoadPreloaded("preloaded2"); + + // load is just convenience for resolve & load + loader.load("preloaded2", scopedListener2); + + sharedListener.assertDone(); + scopedListener2.assertDone(); + + sharedListener.expectResolveDynamicFailure("dne"); + + TestLibraryLoadingListener scopedListener3 = + new TestLibraryLoadingListener().expectResolveDynamicFailure("dne"); + + // not already loaded - so passes through to underlying resolver + assertThrows(LibraryLoadException.class, () -> loader.load("dne", scopedListener3)); + + sharedListener.assertDone(); + scopedListener3.assertDone(); } @Test public void unsupportedPlatform() { + TestLibraryLoadingListener sharedListener = new TestLibraryLoadingListener(); + PlatformSpec unsupportedOsSpec = TestPlatformSpec.of(UNSUPPORTED_OS, AARCH64); - NativeLoader loader = NativeLoader.builder().platformSpec(unsupportedOsSpec).build(); + NativeLoader loader = + NativeLoader.builder().platformSpec(unsupportedOsSpec).addListener(sharedListener).build(); + + assertFalse(loader.isPlatformSupported()); + sharedListener.expectResolveDynamicFailure("dummy"); + + // short-circuit fails during resolution because os isn't supported assertThrows(LibraryLoadException.class, () -> loader.resolveDynamic("dummy")); + + sharedListener.assertDone(); } @Test @@ -64,17 +130,79 @@ public void unsupportArch() { PlatformSpec unsupportedOsSpec = TestPlatformSpec.of(LINUX, UNSUPPORTED_ARCH); NativeLoader loader = NativeLoader.builder().platformSpec(unsupportedOsSpec).build(); - assertThrows(LibraryLoadException.class, () -> loader.resolveDynamic("dummy")); + assertFalse(loader.isPlatformSupported()); + + TestLibraryLoadingListener scopedListener = + new TestLibraryLoadingListener().expectResolveDynamicFailure("dummy"); + + // short-circuit fails during resolution because arch isn't supported + assertThrows(LibraryLoadException.class, () -> loader.resolveDynamic("dummy", scopedListener)); + + scopedListener.assertDone(); } @Test public void loadFailure() throws LibraryLoadException { - NativeLoader loader = NativeLoader.builder().build(); + TestLibraryLoadingListener sharedListener = new TestLibraryLoadingListener(); + + NativeLoader loader = NativeLoader.builder().addListener(sharedListener).build(); + assumeTrue(loader.isPlatformSupported()); + + sharedListener.expectResolveDynamic("dummy"); + sharedListener.expectLoadFailure("dummy"); + + TestLibraryLoadingListener scopedListener = + new TestLibraryLoadingListener().expectResolveDynamic("dummy").expectLoadFailure("dummy"); // test libraries are just text files, so they shouldn't load & link properly // NativeLoader is supposed to wrap the loading failures, so that we // remember to handle them + + // on supported platforms, there is a dummy library file, so this will resolve but fail to load + // & link + assertThrows(LibraryLoadException.class, () -> loader.load("dummy", scopedListener)); + } + + @Test + public void resolutionFailure_in_LibraryResolver() { + Exception exception = new Exception("boom!"); + + NativeLoader loader = + NativeLoader.builder() + .libResolver( + (pathLocator, platformSpec, component, libName) -> { + throw exception; + }) + .build(); + + TestLibraryLoadingListener scopedListener = + new TestLibraryLoadingListener().expectResolveDynamicFailure("dummy", exception); + + assertThrows(LibraryLoadException.class, () -> loader.load("dummy", scopedListener)); + + scopedListener.assertDone(); + } + + @Test + public void resolutionFailure_in_PathLocator() { + TestLibraryLoadingListener sharedListener = new TestLibraryLoadingListener(); + + Exception exception = new Exception("boom!"); + + NativeLoader loader = + NativeLoader.builder() + .addListener(sharedListener) + .pathLocator( + (comp, path) -> { + throw exception; + }) + .build(); + + sharedListener.expectResolveDynamicFailure("dummy", exception); + assertThrows(LibraryLoadException.class, () -> loader.load("dummy")); + + sharedListener.assertDone(); } @Test @@ -92,24 +220,36 @@ public void fromDir() throws LibraryLoadException { @Test public void fromDir_override_windows() throws LibraryLoadException { - NativeLoader loader = NativeLoader.builder().fromDir("test-data").build(); + TestLibraryLoadingListener sharedListener = new TestLibraryLoadingListener(); + + NativeLoader loader = + NativeLoader.builder().fromDir("test-data").addListener(sharedListener).build(); + + sharedListener.expectResolveDynamic(TestPlatformSpec.windows(), "dummy"); try (LibFile lib = loader.resolveDynamic(TestPlatformSpec.windows(), "dummy")) { // loaded directly from directory, so no clean-up required assertRegularFile(lib); assertTrue(lib.getAbsolutePath().endsWith("dummy.dll")); } + + sharedListener.assertDone(); } @Test public void fromDir_override_mac() throws LibraryLoadException { NativeLoader loader = NativeLoader.builder().fromDir("test-data").build(); - try (LibFile lib = loader.resolveDynamic(TestPlatformSpec.mac(), "dummy")) { + TestLibraryLoadingListener scopedListener = + new TestLibraryLoadingListener().expectResolveDynamic(TestPlatformSpec.mac(), "dummy"); + + try (LibFile lib = loader.resolveDynamic(TestPlatformSpec.mac(), "dummy", scopedListener)) { // loaded directly from directory, so no clean-up required assertRegularFile(lib); assertTrue(lib.getAbsolutePath().endsWith("libdummy.dylib")); } + + scopedListener.assertDone(); } @Test @@ -135,17 +275,45 @@ public void fromDirList() throws LibraryLoadException { @Test public void fromDir_with_component() throws LibraryLoadException { - NativeLoader loader = NativeLoader.builder().fromDir("test-data").build(); + TestLibraryLoadingListener sharedListener = new TestLibraryLoadingListener(); + + NativeLoader loader = + NativeLoader.builder().fromDir("test-data").addListener(sharedListener).build(); + + sharedListener.expectResolveDynamic("comp1", "dummy"); try (LibFile lib = loader.resolveDynamic("comp1", "dummy")) { assertRegularFile(lib); assertTrue(lib.getAbsolutePath().contains("comp1")); } + sharedListener.assertDone(); + + sharedListener.expectResolveDynamic("comp2", "dummy"); + try (LibFile lib = loader.resolveDynamic("comp2", "dummy")) { assertRegularFile(lib); assertTrue(lib.getAbsolutePath().contains("comp2")); } + + sharedListener.assertDone(); + } + + @Test + public void fromDir_load_with_component() { + NativeLoader loader = NativeLoader.builder().fromDir("test-data").build(); + + // lib file is a dummy, so fails during loading and linking + assertThrows(LibraryLoadException.class, () -> loader.load("comp1", "dummy")); + + TestLibraryLoadingListener scopedListener2 = + new TestLibraryLoadingListener() + .expectResolveDynamic("comp2", "dummy") + .expectLoadFailure("comp2", "dummy"); + + assertThrows(LibraryLoadException.class, () -> loader.load("comp2", "dummy", scopedListener2)); + + scopedListener2.assertDone(); } @Test @@ -218,10 +386,47 @@ public void fromJarBackedClassLoader() throws IOException, LibraryLoadException try { try (URLClassLoader classLoader = createClassLoader(jar)) { NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader).build(); - try (LibFile lib = loader.resolveDynamic("dummy")) { + + TestLibraryLoadingListener scopedListener = + new TestLibraryLoadingListener() + .expectResolveDynamic("dummy") + .expectTempFileCreated("dummy") + .expectTempFileCleanup("dummy"); + + try (LibFile lib = loader.resolveDynamic("dummy", scopedListener)) { // loaded from a jar, so copied to temp file assertTempFile(lib); } + + scopedListener.assertDone(); + } + } finally { + deleteHelper(jar); + } + } + + @Test + public void fromJarBackedClassLoader_load_with_component() + throws IOException, LibraryLoadException { + Path jar = jar("test-data"); + try { + try (URLClassLoader classLoader = createClassLoader(jar)) { + NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader).build(); + + // lib file is a dummy, so fails during loading and linking + assertThrows(LibraryLoadException.class, () -> loader.load("comp1", "dummy")); + + TestLibraryLoadingListener scopedListener2 = + new TestLibraryLoadingListener() + .expectResolveDynamic("comp2", "dummy") + .expectTempFileCreated("comp2", "dummy") + .expectLoadFailure("comp2", "dummy") + .expectTempFileCleanup("comp2", "dummy"); + + assertThrows( + LibraryLoadException.class, () -> loader.load("comp2", "dummy", scopedListener2)); + + scopedListener2.assertDone(); } } finally { deleteHelper(jar); @@ -263,8 +468,16 @@ public void fromJarBackedClassLoader_with_unwritable_tempDir() NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader).tempDir(noWriteDir).build(); + TestLibraryLoadingListener scopedListener = + new TestLibraryLoadingListener() + .expectResolveDynamic("dummy") + .expectTempFileCreationFailure("dummy"); + // unable to resolve to a File because tempDir isn't writable - assertThrows(LibraryLoadException.class, () -> loader.resolveDynamic("dummy")); + assertThrows( + LibraryLoadException.class, () -> loader.resolveDynamic("dummy", scopedListener)); + + scopedListener.assertDone(); } finally { deleteHelper(noWriteDir); } diff --git a/components/native-loader/src/test/java/datadog/nativeloader/TestLibraryLoadingListener.java b/components/native-loader/src/test/java/datadog/nativeloader/TestLibraryLoadingListener.java new file mode 100644 index 00000000000..9996de25a26 --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/TestLibraryLoadingListener.java @@ -0,0 +1,535 @@ +package datadog.nativeloader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.net.URL; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedList; + +public final class TestLibraryLoadingListener implements LibraryLoadingListener { + private final LinkedList checks; + + private Check failedCheck = null; + private Throwable failedCause = null; + + // By design, listeners are supposed to receive the underlying cause not a LibraryLoadException + // directly + static final ThrowableCheck NOT_LIB_LOAD_EXCEPTION = + (t) -> { + assertFalse( + t instanceof LibraryLoadException, + "LibraryLoadException - instead of underlying cause"); + }; + + public TestLibraryLoadingListener() { + this.checks = new LinkedList<>(); + } + + private TestLibraryLoadingListener(TestLibraryLoadingListener that) { + this.checks = new LinkedList<>(that.checks); + } + + public TestLibraryLoadingListener expectResolveDynamic(String expectedLibName) { + return this.expectResolveDynamic(new LibCheck(expectedLibName)); + } + + public TestLibraryLoadingListener expectResolveDynamic( + String expectedComponent, String expectedLibName) { + return this.expectResolveDynamic(new LibCheck(expectedComponent, expectedLibName)); + } + + public TestLibraryLoadingListener expectResolveDynamic( + PlatformSpec expectedPlatformSpec, String expectedLibName) { + return this.expectResolveDynamic(new LibCheck(expectedPlatformSpec, expectedLibName)); + } + + private TestLibraryLoadingListener expectResolveDynamic(LibCheck libCheck) { + return this.addCheck( + new Check("onResolveDynamic %s", libCheck) { + @Override + public void onResolveDynamic( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + URL optionalUrl) { + libCheck.assertMatches(platformSpec, optionalComponent, libName); + } + }); + } + + public TestLibraryLoadingListener expectResolvePreloaded(String expectedLibName) { + return this.expectResolvePreloaded(new LibCheck(expectedLibName)); + } + + private TestLibraryLoadingListener expectResolvePreloaded(LibCheck libCheck) { + return this.addCheck( + new Check("onResolveDynamic:preloaded %s", libCheck) { + @Override + public void onResolveDynamic( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + URL optionalUrl) { + libCheck.assertMatches(platformSpec, optionalComponent, libName); + assertTrue(isPreloaded); + } + }); + } + + public TestLibraryLoadingListener expectResolveDynamicFailure(String expectedLibName) { + return this.expectResolveDynamicFailure(new LibCheck(expectedLibName)); + } + + public TestLibraryLoadingListener expectResolveDynamicFailure( + String expectedLibName, Throwable expectedThrowable) { + return this.expectResolveDynamicFailure( + new LibCheck(expectedLibName), (t) -> assertSame(expectedThrowable, t)); + } + + private TestLibraryLoadingListener expectResolveDynamicFailure(LibCheck libCheck) { + return this.expectResolveDynamicFailure(libCheck, NOT_LIB_LOAD_EXCEPTION); + } + + private TestLibraryLoadingListener expectResolveDynamicFailure( + LibCheck libCheck, ThrowableCheck throwableCheck) { + return this.addCheck( + new Check("onResolveDynamicFailure %s", libCheck) { + @Override + public void onResolveDynamicFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Throwable optionalCause) { + libCheck.assertMatches(platformSpec, optionalComponent, libName); + throwableCheck.assertMatches(optionalCause); + } + }); + } + + public TestLibraryLoadingListener expectLoad(String expectedLibName) { + return this.expectLoad(new LibCheck(expectedLibName)); + } + + public TestLibraryLoadingListener expectLoad(String expectedComponent, String expectedLibName) { + return this.expectLoad(new LibCheck(expectedComponent, expectedLibName)); + } + + private TestLibraryLoadingListener expectLoad(LibCheck libCheck) { + return this.addCheck( + new Check("onLoad %s", libCheck) { + @Override + public void onLoad( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + Path optionalLibPath) { + libCheck.assertMatches(platformSpec, optionalComponent, libName); + } + }); + } + + public TestLibraryLoadingListener expectLoadPreloaded(String expectedLibName) { + return this.expectLoadPreloaded(new LibCheck(expectedLibName)); + } + + private TestLibraryLoadingListener expectLoadPreloaded(LibCheck libCheck) { + return this.addCheck( + new Check("onLoad:preloaded %s", libCheck) { + @Override + public void onLoad( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + Path optionalLibPath) { + libCheck.assertMatches(platformSpec, optionalComponent, libName); + assertTrue(isPreloaded); + } + }); + } + + public TestLibraryLoadingListener expectLoadFailure(String expectedLibName) { + return this.expectLoadFailure(new LibCheck(expectedLibName)); + } + + public TestLibraryLoadingListener expectLoadFailure( + String expectedComponent, String expectedLibName) { + return this.expectLoadFailure(new LibCheck(expectedComponent, expectedLibName)); + } + + private TestLibraryLoadingListener expectLoadFailure(LibCheck libCheck) { + return this.expectLoadFailure(libCheck, NOT_LIB_LOAD_EXCEPTION); + } + + private TestLibraryLoadingListener expectLoadFailure( + LibCheck libCheck, ThrowableCheck throwableCheck) { + return this.addCheck( + new Check("onLoadFailure %s", libCheck) { + @Override + public void onLoadFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Throwable optionalCause) { + libCheck.assertMatches(platformSpec, optionalComponent, libName); + throwableCheck.assertMatches(optionalCause); + } + }); + } + + public TestLibraryLoadingListener expectTempFileCreated(String expectedLibName) { + return this.expectTempFileCreated(new LibCheck(expectedLibName)); + } + + public TestLibraryLoadingListener expectTempFileCreated( + String expectedComponent, String expectedLibName) { + return this.expectTempFileCreated(new LibCheck(expectedComponent, expectedLibName)); + } + + private TestLibraryLoadingListener expectTempFileCreated(LibCheck libCheck) { + return this.addCheck( + new Check("onTempFileCreated %s", libCheck) { + @Override + public void onTempFileCreated( + PlatformSpec platformSpec, String optionalComponent, String libName, Path tempFile) { + libCheck.assertMatches(platformSpec, optionalComponent, libName); + assertNotNull(tempFile); + } + }); + } + + public TestLibraryLoadingListener expectTempFileCreationFailure(String expectedLibName) { + return this.expectTempFileCreationFailure(new LibCheck(expectedLibName)); + } + + private TestLibraryLoadingListener expectTempFileCreationFailure(LibCheck libCheck) { + return this.expectTempFileCreationFailure(libCheck, NOT_LIB_LOAD_EXCEPTION); + } + + private TestLibraryLoadingListener expectTempFileCreationFailure( + LibCheck libCheck, ThrowableCheck throwableCheck) { + return this.addCheck( + new Check("onTempFileCreationFailure %s", libCheck) { + @Override + public void onTempFileCreationFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Path tempDir, + String libExt, + Throwable optionalCause) { + libCheck.assertMatches(platformSpec, optionalComponent, libName); + assertNotNull(tempDir); + assertNotNull(libExt); + } + }); + } + + public TestLibraryLoadingListener expectTempFileCleanup(String expectedLibName) { + return this.expectTempFileCleanup(new LibCheck(expectedLibName)); + } + + public TestLibraryLoadingListener expectTempFileCleanup( + String expectedComponent, String expectedLibName) { + return this.expectTempFileCleanup(new LibCheck(expectedComponent, expectedLibName)); + } + + TestLibraryLoadingListener expectTempFileCleanup(LibCheck libCheck) { + return this.addCheck( + new Check("onTempFileCreationCleanup %s", libCheck) { + @Override + public void onTempFileCleanup( + PlatformSpec platformSpec, String optionalComponent, String libName, Path tempFile) { + libCheck.assertMatches(platformSpec, optionalComponent, libName); + assertNotNull(tempFile); + } + }); + } + + public TestLibraryLoadingListener copy() { + return new TestLibraryLoadingListener(this); + } + + public void assertDone() { + if (this.failedCheck != null) { + try { + fail("check failed: " + this.failedCheck, this.failedCause); + } catch (AssertionError e) { + e.initCause(this.failedCause); + + throw e; + } + } + + // written this way for better debugging + assertEquals(Collections.emptyList(), this.checks); + } + + @Override + public void onResolveDynamic( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + URL optionalUrl) { + this.nextCheck( + check -> + check.onResolveDynamic( + platformSpec, optionalComponent, libName, isPreloaded, optionalUrl)); + } + + @Override + public void onResolveDynamicFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Throwable optionalCause) { + this.nextCheck( + check -> + check.onResolveDynamicFailure(platformSpec, optionalComponent, libName, optionalCause)); + } + + @Override + public void onLoad( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + Path optionalLibPath) { + this.nextCheck( + check -> + check.onLoad(platformSpec, optionalComponent, libName, isPreloaded, optionalLibPath)); + } + + @Override + public void onLoadFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Throwable optionalCause) { + this.nextCheck( + check -> check.onLoadFailure(platformSpec, optionalComponent, libName, optionalCause)); + } + + @Override + public void onTempFileCreated( + PlatformSpec platformSpec, String optionalComponent, String libName, Path tempFile) { + this.nextCheck( + check -> check.onTempFileCreated(platformSpec, optionalComponent, libName, tempFile)); + } + + @Override + public void onTempFileCreationFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Path tempDir, + String libExt, + Throwable optionalCause) { + this.nextCheck( + check -> + check.onTempFileCreationFailure( + platformSpec, optionalComponent, libName, tempDir, libExt, optionalCause)); + } + + @Override + public void onTempFileCleanup( + PlatformSpec platformSpec, String optionalComponent, String libName, Path tempFile) { + this.nextCheck( + check -> check.onTempFileCleanup(platformSpec, optionalComponent, libName, tempFile)); + } + + public TestLibraryLoadingListener addCheck(Check check) { + this.checks.addLast(check); + return this; + } + + private void nextCheck(CheckInvocation invocation) { + Check nextCheck = this.checks.isEmpty() ? Check.NOTHING : this.checks.removeFirst(); + try { + invocation.invoke(nextCheck); + } catch (Throwable t) { + if (this.failedCheck == null) { + this.failedCheck = nextCheck; + this.failedCause = t; + } + } + } + + public abstract static class Check implements LibraryLoadingListener { + static final Check NOTHING = new Check("nothing") {}; + + private final String name; + + Check(String nameFormat, Object... nameArgs) { + this(String.format(nameFormat, nameArgs)); + } + + Check(String name) { + this.name = name; + } + + @Override + public void onLoad( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + Path optionalLibPath) { + this.fallback( + "onLoad", platformSpec, optionalComponent, libName, isPreloaded, optionalLibPath); + } + + @Override + public void onLoadFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Throwable optionalCause) { + this.fallback("onLoadFailure", platformSpec, optionalComponent, libName, optionalCause); + } + + @Override + public void onResolveDynamic( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + URL optionalUrl) { + this.fallback( + "onResolveDynamic", platformSpec, optionalComponent, libName, isPreloaded, optionalUrl); + } + + @Override + public void onResolveDynamicFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Throwable optionalCause) { + this.fallback( + "onResolveDynamicFailure", platformSpec, optionalComponent, libName, optionalCause); + } + + @Override + public void onTempFileCreated( + PlatformSpec platformSpec, String optionalComponent, String libName, Path tempFile) { + this.fallback("onTmepFileCreated", platformSpec, optionalComponent, libName, tempFile); + } + + @Override + public void onTempFileCreationFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Path tempDir, + String libExt, + Throwable optionalCause) { + this.fallback( + "onTempFileCreationFailure", + platformSpec, + optionalComponent, + libName, + tempDir, + libExt, + optionalCause); + } + + @Override + public void onTempFileCleanup( + PlatformSpec platformSpec, String optionalComponent, String libName, Path tempFile) { + this.fallback("onTempFileCleanup", platformSpec, optionalComponent, libName, tempFile); + } + + void fallback(String methodName, Object... args) { + fail("unxpected call: " + callToString(methodName, args) + " - expected: " + this.name); + } + + static final String callToString(String methodName, Object... args) { + StringBuilder builder = new StringBuilder(); + builder.append(methodName); + builder.append('('); + for (int i = 0; i < args.length; ++i) { + if (i != 0) builder.append(", "); + builder.append(String.valueOf(args[i])); + } + builder.append(')'); + return builder.toString(); + } + + @Override + public String toString() { + return this.name; + } + } + + @FunctionalInterface + interface CheckInvocation { + void invoke(Check check); + } + + static final class LibCheck { + private final PlatformSpec expectedPlatformSpec; + private final String expectedComponent; + private final String expectedLibName; + + LibCheck(PlatformSpec expectedPlatformSpec, String expectedLibName) { + this(null, null, expectedLibName); + } + + LibCheck(String expectedComponent, String expectedLibName) { + this(null, expectedComponent, expectedLibName); + } + + LibCheck(String expectedLibName) { + this(null, null, expectedLibName); + } + + LibCheck(PlatformSpec expectedPlatformSpec, String expectedComponent, String expectedLibName) { + this.expectedPlatformSpec = expectedPlatformSpec; + this.expectedComponent = expectedComponent; + this.expectedLibName = expectedLibName; + } + + void assertMatches(PlatformSpec platformSpec, String optionalComponent, String libName) { + if (this.expectedPlatformSpec == null) { + // if no expectedPlatformSpec was provided -- just check that platformSpec is not null + assertNotNull(platformSpec); + } else { + assertEquals(this.expectedPlatformSpec, platformSpec); + } + + if (this.expectedComponent == null) { + // a null expectedComponent is treated as not expecting a component + assertNull(optionalComponent); + } else { + assertEquals(this.expectedComponent, optionalComponent); + } + + assertEquals(this.expectedLibName, libName); + } + + @Override + public String toString() { + if (this.expectedComponent == null) { + return this.expectedLibName; + } else { + return this.expectedComponent + "/" + this.expectedLibName; + } + } + } + + @FunctionalInterface + interface ThrowableCheck { + void assertMatches(Throwable t); + } +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/ThrowingLibraryLoadingListener.java b/components/native-loader/src/test/java/datadog/nativeloader/ThrowingLibraryLoadingListener.java new file mode 100644 index 00000000000..75f0a2ed6fc --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/ThrowingLibraryLoadingListener.java @@ -0,0 +1,71 @@ +package datadog.nativeloader; + +import java.net.URL; +import java.nio.file.Path; + +public class ThrowingLibraryLoadingListener implements LibraryLoadingListener { + @Override + public final void onLoad( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + Path optionalLibPath) { + this.throwException("load"); + } + + @Override + public final void onLoadFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Throwable optionalCause) { + this.throwException("loadFailure"); + } + + @Override + public final void onResolveDynamic( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + boolean isPreloaded, + URL optionalUrl) { + this.throwException("resolveDynamic"); + } + + @Override + public final void onResolveDynamicFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Throwable optionalCause) { + this.throwException("resolveDynamicFailure"); + } + + @Override + public final void onTempFileCreated( + PlatformSpec platformSpec, String optionalComponent, String libName, Path tempFile) { + this.throwException("tempFileCreated"); + } + + @Override + public final void onTempFileCreationFailure( + PlatformSpec platformSpec, + String optionalComponent, + String libName, + Path tempDir, + String libExt, + Throwable optionalCause) { + this.throwException("tempFileCreationFailure"); + } + + @Override + public final void onTempFileCleanup( + PlatformSpec platformSpec, String optionalComponent, String libName, Path tempFile) { + this.throwException("tempFileCleanup"); + } + + void throwException(String event) { + throw new RuntimeException(event); + } +}