Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inject coverage store field into Thread class #7284

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
@@ -1,25 +1,60 @@
package datadog.trace.civisibility.config;

import datadog.trace.civisibility.ipc.Serializer;
import de.thetaphi.forbiddenapis.SuppressForbidden;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JvmInfo {

private static final Logger LOGGER =
LoggerFactory.getLogger(ModuleExecutionSettingsFactoryImpl.class);

public static final JvmInfo CURRENT_JVM =
new JvmInfo(
System.getProperty("java.runtime.name"),
System.getProperty("java.version"),
System.getProperty("java.vendor"));
System.getProperty("java.class.version"),
System.getProperty("java.vendor"),
System.getProperty("java.home"));

private static final int JAVA_8_CLASS_VERSION = 52;

private final String name;
private final String version;
private final int majorClassVersion;
private final String vendor;
private final Path home;

@SuppressForbidden // split on "\." uses fast path
public JvmInfo(String name, String version, String classVersion, String vendor, String home) {
this.name = name;
this.version = version;
this.vendor = vendor;
this.home = home != null ? Paths.get(home) : null;

int majorClassVersion = -1;
try {
if (classVersion != null) {
String[] classVersionTokens = classVersion.split("\\.");
majorClassVersion = Integer.parseInt(classVersionTokens[0]);
}
} catch (Exception e) {
LOGGER.debug("Could not parse class version {} for JVM {}", classVersion, this);
}
this.majorClassVersion = majorClassVersion;
}

public JvmInfo(String name, String version, String vendor) {
private JvmInfo(String name, String version, int majorClassVersion, String vendor, Path home) {
this.name = name;
this.version = version;
this.vendor = vendor;
this.home = home;
this.majorClassVersion = majorClassVersion;
}

public String getName() {
Expand All @@ -30,10 +65,22 @@ public String getVersion() {
return version;
}

public int getMajorClassVersion() {
return majorClassVersion;
}

public String getVendor() {
return vendor;
}

public Path getHome() {
return home;
}

public boolean isModular() {
return majorClassVersion > JAVA_8_CLASS_VERSION;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -45,12 +92,14 @@ public boolean equals(Object o) {
JvmInfo jvmInfo = (JvmInfo) o;
return Objects.equals(name, jvmInfo.name)
&& Objects.equals(version, jvmInfo.version)
&& Objects.equals(vendor, jvmInfo.vendor);
&& Objects.equals(majorClassVersion, jvmInfo.majorClassVersion)
&& Objects.equals(vendor, jvmInfo.vendor)
&& Objects.equals(home, jvmInfo.home);
}

@Override
public int hashCode() {
return Objects.hash(name, version, vendor);
return Objects.hash(name, version, majorClassVersion, vendor, home);
}

@Override
Expand All @@ -61,21 +110,33 @@ public String toString() {
+ '\''
+ ", version='"
+ version
+ ", majorClassVersion='"
+ majorClassVersion
+ '\''
+ ", vendor='"
+ vendor
+ '\''
+ ", home='"
+ home
+ '\''
+ '}';
}

public static void serialize(Serializer serializer, JvmInfo jvmInfo) {
serializer.write(jvmInfo.name);
serializer.write(jvmInfo.version);
serializer.write(jvmInfo.majorClassVersion);
serializer.write(jvmInfo.vendor);
serializer.write(jvmInfo.home != null ? String.valueOf(jvmInfo.home) : null);
}

public static JvmInfo deserialize(ByteBuffer buf) {
String name = Serializer.readString(buf);
String version = Serializer.readString(buf);
int majorClassVersion = Serializer.readInt(buf);
String vendor = Serializer.readString(buf);
String home = Serializer.readString(buf);
return new JvmInfo(
Serializer.readString(buf), Serializer.readString(buf), Serializer.readString(buf));
name, version, majorClassVersion, vendor, home != null ? Paths.get(home) : null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ private static final class JvmVersionOutputParser
public JvmInfo parse(InputStream inputStream) throws IOException {
String name = null;
String version = null;
String classVersion = null;
String vendor = null;
String home = null;

BufferedReader bis =
new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
Expand All @@ -74,11 +76,15 @@ public JvmInfo parse(InputStream inputStream) throws IOException {
name = getPropertyValue(line);
} else if (line.contains("java.version ")) {
version = getPropertyValue(line);
} else if (line.contains("java.class.version ")) {
classVersion = getPropertyValue(line);
} else if (line.contains("java.vendor ")) {
vendor = getPropertyValue(line);
} else if (line.contains("java.home ")) {
home = getPropertyValue(line);
}
}
return new JvmInfo(name, version, vendor);
return new JvmInfo(name, version, classVersion, vendor, home);
}

private String getPropertyValue(String line) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
import datadog.trace.api.config.CiVisibilityConfig;
import datadog.trace.api.git.GitInfo;
import datadog.trace.api.git.GitInfoProvider;
import datadog.trace.civisibility.coverage.instrumentation.store.JvmPatcher;
import datadog.trace.civisibility.git.tree.GitDataUploader;
import datadog.trace.civisibility.source.index.RepoIndex;
import datadog.trace.civisibility.source.index.RepoIndexProvider;
import datadog.trace.civisibility.utils.FileUtils;
import datadog.trace.util.Strings;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -65,14 +68,6 @@ public ModuleExecutionSettings create(JvmInfo jvmInfo, @Nullable String moduleNa
boolean testSkippingEnabled = isTestSkippingEnabled(ciVisibilitySettings);
boolean flakyTestRetriesEnabled = isFlakyTestRetriesEnabled(ciVisibilitySettings);
boolean earlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled(ciVisibilitySettings);
Map<String, String> systemProperties =
getPropertiesPropagatedToChildProcess(
itrEnabled,
codeCoverageEnabled,
testSkippingEnabled,
flakyTestRetriesEnabled,
earlyFlakeDetectionEnabled);

LOGGER.info(
"CI Visibility settings ({}, {}):\n"
+ "Intelligent Test Runner - {},\n"
Expand Down Expand Up @@ -109,6 +104,24 @@ public ModuleExecutionSettings create(JvmInfo jvmInfo, @Nullable String moduleNa
Map<String, Collection<TestIdentifier>> knownTestsByModuleName =
earlyFlakeDetectionEnabled ? getKnownTests(tracerEnvironment) : null;

Map<String, String> systemProperties =
getPropertiesPropagatedToChildProcess(
itrEnabled,
codeCoverageEnabled,
testSkippingEnabled,
flakyTestRetriesEnabled,
earlyFlakeDetectionEnabled);

List<String> jvmOptions = new ArrayList<>();
if (codeCoverageEnabled
&& config.isCiVisibilityJvmPatchingEnabled()
// null home or negative class version means we couldn't get JVM details that are necessary
// for patching
&& jvmInfo.getHome() != null
&& jvmInfo.getMajorClassVersion() > 0) {
jvmOptions.addAll(patchJvm(jvmInfo));
}

List<String> coverageEnabledPackages = getCoverageEnabledPackages(codeCoverageEnabled);
return new ModuleExecutionSettings(
itrEnabled,
Expand All @@ -123,13 +136,38 @@ public ModuleExecutionSettings create(JvmInfo jvmInfo, @Nullable String moduleNa
? ciVisibilitySettings.getEarlyFlakeDetectionSettings()
: EarlyFlakeDetectionSettings.DEFAULT,
systemProperties,
jvmOptions,
itrCorrelationId,
skippableTestsByModuleName,
flakyTests,
knownTestsByModuleName,
coverageEnabledPackages);
}

/**
* @return The list of JVM options that are needed to prepend the patch to the bootstrap classpath
*/
private List<String> patchJvm(JvmInfo jvmInfo) {
try {
JvmPatcher patcher = new JvmPatcher(jvmInfo);
Path patchPath = patcher.createPatch();

Thread cleanupPatch =
new Thread(() -> FileUtils.deleteSafely(patchPath), "dd-ci-vis-patched-classes-remove");
Runtime.getRuntime().addShutdownHook(cleanupPatch);

if (jvmInfo.isModular()) {
return Arrays.asList("--patch-module", String.format("java.base=%s", patchPath));
} else {
return Collections.singletonList(String.format("-Xbootclasspath/p:%s", patchPath));
}

} catch (Exception e) {
LOGGER.debug("Failed to patch {}", jvmInfo, e);
return Collections.emptyList();
}
}

private TracerEnvironment buildTracerEnvironment(
String repositoryRoot, JvmInfo jvmInfo, @Nullable String moduleName) {
GitInfo gitInfo = GitInfoProvider.INSTANCE.getGitInfo(repositoryRoot);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static ByteBuffer serialize(ModuleExecutionSettings settings) {
EarlyFlakeDetectionSettingsSerializer.serialize(s, settings.getEarlyFlakeDetectionSettings());

s.write(settings.getSystemProperties());
s.write(settings.getJvmOptions());
s.write(settings.getItrCorrelationId());
s.write(
settings.getSkippableTestsByModule(),
Expand All @@ -56,6 +57,8 @@ public static ModuleExecutionSettings deserialize(ByteBuffer buffer) {
EarlyFlakeDetectionSettingsSerializer.deserialize(buffer);

Map<String, String> systemProperties = Serializer.readStringMap(buffer);
List<String> jvmOptions = Serializer.readStringList(buffer);

String itrCorrelationId = Serializer.readString(buffer);
Map<String, Collection<TestIdentifier>> skippableTestsByModule =
Serializer.readMap(
Expand All @@ -78,6 +81,7 @@ public static ModuleExecutionSettings deserialize(ByteBuffer buffer) {
flakyTestRetriesEnabled,
earlyFlakeDetectionSettings,
systemProperties,
jvmOptions,
itrCorrelationId,
skippableTestsByModule,
flakyTests,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package datadog.trace.civisibility.coverage.instrumentation.store;

import datadog.communication.util.IOThrowingFunction;
import datadog.trace.civisibility.config.JvmInfo;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class CoreJvmClassReader {

private static final String JAVA_BASE_MODULE_RELATIVE_PATH = "jmods/java.base.jmod";

// java.home contains the "/jre" segment for Java 8 distributions
private static final String RT_JAR_RELATIVE_PATH = "lib/rt.jar";

/**
* Performs an action on the bytecode of a core JVM class
*
* @param jvm The JVM distribution
* @param className The name of the class (as returned by {@link Class#getName}, e.g. {@code
* java.lang.Thread})
* @param action The action to be performed on the class bytecode stream
* @return The return value of the action
* @param <T> The type of the return value
* @throws IOException If the stream could not be retrieved
*/
public <T> T withClassStream(
JvmInfo jvm, String className, IOThrowingFunction<InputStream, T> action) throws IOException {
if (jvm.isModular()) {
return withClassStreamModularJdk(jvm.getHome(), className, action);
} else {
return withClassStreamPreModularJdk(jvm.getHome(), className, action);
}
}

private <T> T withClassStreamModularJdk(
Path jvmHome, String className, IOThrowingFunction<InputStream, T> action)
throws IOException {
Path javaBaseModule = jvmHome.resolve(JAVA_BASE_MODULE_RELATIVE_PATH);
return withZipEntry(
javaBaseModule, "classes/" + className.replace('.', '/') + ".class", action);
}

private <T> T withClassStreamPreModularJdk(
Path jvmHome, String className, IOThrowingFunction<InputStream, T> action)
throws IOException {
Path rtJar = jvmHome.resolve(RT_JAR_RELATIVE_PATH);
return withZipEntry(rtJar, className.replace('.', '/') + ".class", action);
}

private static <T> T withZipEntry(
Path zipFilePath, String entryName, IOThrowingFunction<InputStream, T> action)
throws IOException {
try (ZipFile zipFile = new ZipFile(zipFilePath.toFile())) {
ZipEntry entry = zipFile.getEntry(entryName);
if (entry == null) {
throw new IOException("Entry " + entryName + " not found in zip file " + zipFilePath);
}
try (InputStream entryStream = zipFile.getInputStream(entry)) {
return action.apply(entryStream);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package datadog.trace.civisibility.coverage.instrumentation.store;

import static datadog.trace.api.civisibility.coverage.CoverageBridge.COVERAGE_PROBES_FIELD_NAME;

import datadog.trace.api.civisibility.coverage.CoverageBridge;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;

/**
* A {@link ClassVisitor} that injects an instance field named {@link
* CoverageBridge#COVERAGE_PROBES_FIELD_NAME} of type {@link Object} into the visited class
*/
public class CoverageStoreFieldInjector extends ClassVisitor {

public CoverageStoreFieldInjector(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}

@Override
public void visitEnd() {
cv.visitField(Opcodes.ACC_PUBLIC, COVERAGE_PROBES_FIELD_NAME, "Ljava/lang/Object;", null, null)
.visitEnd();
super.visitEnd();
}
}
Loading
Loading