Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package datadog.trace.agent.tooling.muzzle;

import static java.util.Arrays.asList;

import datadog.trace.agent.tooling.AdviceShader;
import datadog.trace.agent.tooling.HelperScanner;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.agent.tooling.InstrumenterModule;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.bytebuddy.dynamic.ClassFileLocator;

/**
* Resolves the helper classes an {@link InstrumenterModule} injects: a module with a manually
* declared {@code helperClassNames()} list uses it directly, otherwise the helpers inferred from
* its advice are used (dependency-ordered, build-time-only classes dropped).
*
* <p>Used by {@link MuzzleGenerator}, which resolves once per module and emits both outputs from
* that single crawl: the {@code $Muzzle} side-class (excluding these helpers from the asserted
* references) and the module's own {@code helperClassNames()} (holding the resolved list).
*/
final class HelperResolver {
private static final String MUZZLE_REFERENCE_API = "datadog/trace/agent/tooling/muzzle/Reference";

private final File sourceDir;

HelperResolver(File sourceDir) {
this.sourceDir = sourceDir;
}

/** The crawled advice references and the resolved helper set for a module. */
static final class Result {
final List<Reference> references;
final Set<String> adviceClasses;
final String[] injectedHelpers;

Result(List<Reference> references, Set<String> adviceClasses, String[] injectedHelpers) {
this.references = references;
this.adviceClasses = adviceClasses;
this.injectedHelpers = injectedHelpers;
}
}

Result resolve(InstrumenterModule module) {
AdviceShader adviceShader = AdviceShader.with(module.adviceShading());

// Collect the muzzle references from every advice the module defines.
Set<String> adviceClasses = new HashSet<>();
List<Reference> allReferences = new ArrayList<>();
for (Instrumenter instrumenter : module.typeInstrumentations()) {
if (instrumenter instanceof Instrumenter.HasMethodAdvice) {
Collections.addAll(
allReferences,
generateReferences(
(Instrumenter.HasMethodAdvice) instrumenter, adviceShader, adviceClasses));
}
}
return new Result(
allReferences, adviceClasses, computeInjectedHelpers(module, allReferences, adviceClasses));
}

private static Reference[] generateReferences(
Instrumenter.HasMethodAdvice instrumenter,
AdviceShader adviceShader,
Set<String> allAdviceClasses) {
// track sources we've generated references from to avoid recursion
final Set<String> referenceSources = new HashSet<>();
final Map<String, Reference> references = new LinkedHashMap<>();
final Set<String> adviceClasses = new HashSet<>();
instrumenter.methodAdvice(
(matcher, adviceClass, additionalClasses) -> {
adviceClasses.add(adviceClass);
if (additionalClasses != null) {
adviceClasses.addAll(asList(additionalClasses));
}
});
// remember the advice roots so callers can exclude them from the injected helper set
allAdviceClasses.addAll(adviceClasses);
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
for (String adviceClass : adviceClasses) {
if (referenceSources.add(adviceClass)) {
for (Map.Entry<String, Reference> entry :
ReferenceCreator.createReferencesFrom(adviceClass, adviceShader, contextClassLoader)
.entrySet()) {
Reference toMerge = references.get(entry.getKey());
if (null == toMerge) {
references.put(entry.getKey(), entry.getValue());
} else {
references.put(entry.getKey(), toMerge.merge(entry.getValue()));
}
}
}
}
return references.values().toArray(new Reference[0]);
}

/** Resolves the ordered set of helper classes to inject for a module. */
String[] computeInjectedHelpers(
InstrumenterModule module, List<Reference> allReferences, Set<String> adviceClasses) {
// A module that declares its own helper list uses it directly.
Set<String> manualHelpers = new LinkedHashSet<>(asList(module.helperClassNames()));
if (!manualHelpers.isEmpty()) {
return manualHelpers.toArray(new String[0]);
}

// Otherwise infer them: our classes referenced from the advice (minus the advice roots), plus
// their nested classes, dependency-ordered, with build-time-only muzzle providers dropped.
HelperClassPredicate helperPredicate = new HelperClassPredicate(this::isOwnOutput);
Set<String> helpers = new LinkedHashSet<>();
for (Reference reference : allReferences) {
if (!adviceClasses.contains(reference.className)
&& helperPredicate.isHelperClass(reference.className)) {
helpers.add(reference.className);
}
}
for (String helper : new ArrayList<>(helpers)) {
if (isOwnOutput(helper)) {
addNestedClasses(helper, helpers);
}
}
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
String[] orderedHelpers = discoverAndOrderHelpers(helpers, helperPredicate, contextClassLoader);
ClassFileLocator locator = ClassFileLocator.ForClassLoader.of(contextClassLoader);
List<String> injectable = new ArrayList<>(orderedHelpers.length);
for (String helper : orderedHelpers) {
if (!isBuildTimeOnly(helper, locator)) {
injectable.add(helper);
}
}
return injectable.toArray(new String[0]);
}

/** {@code true} if the class was compiled from this instrumentation subproject's own output. */
private boolean isOwnOutput(String className) {
return new File(sourceDir, className.replace('.', '/') + ".class").isFile();
}

/** Adds the nested classes ({@code Foo$Bar}, {@code Foo$1}, ...) of an ownOutput helper. */
private void addNestedClasses(String className, Set<String> helperClasses) {
File classFile = new File(sourceDir, className.replace('.', '/') + ".class");
File dir = classFile.getParentFile();
if (dir == null || !dir.isDirectory()) {
return;
}
int lastDot = className.lastIndexOf('.');
String pkg = lastDot < 0 ? "" : className.substring(0, lastDot + 1);
String prefix = (lastDot < 0 ? className : className.substring(lastDot + 1)) + "$";
File[] siblings = dir.listFiles();
if (siblings == null) {
return;
}
for (File sibling : siblings) {
String fileName = sibling.getName();
if (fileName.startsWith(prefix) && fileName.endsWith(".class")) {
helperClasses.add(pkg + fileName.substring(0, fileName.length() - ".class".length()));
}
}
}

/**
* {@code true} if the class uses the muzzle {@link Reference} API (as a {@link ReferenceProvider}
* or via {@code compileReferences}) - this means the class is build-time only and should not be
* injected.
*/
static boolean isBuildTimeOnly(String className, ClassFileLocator locator) {
try {
ClassFileLocator.Resolution resolution = locator.locate(className);
if (!resolution.isResolved()) {
return false;
}
// The muzzle type appears as a constant-pool entry when the class references it.
return new String(resolution.resolve(), StandardCharsets.ISO_8859_1)
.contains(MUZZLE_REFERENCE_API);
} catch (IOException e) {
return false;
}
}

/**
* Expands the given helpers with any helper classes they depend on and returns them in
* dependency-first load order (required by {@link datadog.trace.agent.tooling.HelperInjector})
* via {@link HelperScanner}. Library classes the scanner pulls in are dropped, but helpers that
* could not be located are kept (appended, unordered).
*/
private static String[] discoverAndOrderHelpers(
Set<String> initialHelpers, HelperClassPredicate helperPredicate, ClassLoader loader) {
if (initialHelpers.isEmpty()) {
return new String[0];
}
List<String> ordered = new ArrayList<>();
try {
for (String name :
HelperScanner.withClassDependencies(
ClassFileLocator.ForClassLoader.of(loader), initialHelpers.toArray(new String[0]))) {
if (helperPredicate.isHelperClass(name) && !ordered.contains(name)) {
ordered.add(name);
}
}
} catch (Throwable ignore) {
// best-effort ordering; unlocatable helpers are appended below
}
for (String helper : initialHelpers) {
if (!ordered.contains(helper)) {
ordered.add(helper);
}
}
return ordered.toArray(new String[0]);
}
}
Loading