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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions components/environment/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
plugins {
`java-library`
id("com.gradleup.shadow")
}

apply(from = "$rootDir/gradle/java.gradle")

/*
* Add an addition gradle configuration to be consumed by bootstrap only.
* "datadog.trace." prefix is required to be excluded from Jacoco instrumentation.
* See ConfigDefaults.DEFAULT_CIVISIBILITY_JACOCO_PLUGIN_EXCLUDES for more details.
*/
tasks.shadowJar {
relocate("datadog.environment", "datadog.trace.bootstrap.environment")
}

/*
* Configure test coverage.
*/
extra.set("minimumInstructionCoverage", 0.7)
val excludedClassesCoverage by extra {
listOf(
"datadog.environment.JavaVirtualMachine", // depends on OS and JVM vendor
"datadog.environment.JavaVirtualMachine.JvmOptionsHolder", // depends on OS and JVM vendor
"datadog.environment.JvmOptions", // depends on OS and JVM vendor
"datadog.environment.OperatingSystem", // depends on OS
)
}
val excludedClassesBranchCoverage by extra {
listOf("datadog.environment.CommandLine") // tested using forked process
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package datadog.environment;

import static java.util.Collections.emptyList;

import de.thetaphi.forbiddenapis.SuppressForbidden;
import java.util.Arrays;
import java.util.List;

/**
* Fetches and captures the command line, both command and its arguments. It relies on a
* non-standard {@code sun.java.command} system property and was tested on:
*
* <ul>
* <li>OracleJDK,
* <li>OpenJDK,
* <li>Temurin based JDK,
* <li>IMB JDK,
* <li>Azul Zulu,
* <li>Amazon Coretto,
* </ul>
*
* This should be replaced by {@code ProcessHandle} and {@code ProcessHandle.Info} once Java 9+
* become available.
*/
class CommandLine {
private static final String SUN_JAVA_COMMAND_PROPERTY = "sun.java.command";
final List<String> fullCommand = findFullCommand();
final String name = getCommandName();
final List<String> arguments = getCommandArguments();

@SuppressForbidden // split on single-character uses fast path
private List<String> findFullCommand() {
String command = SystemProperties.getOrDefault(SUN_JAVA_COMMAND_PROPERTY, "").trim();
return command.isEmpty() ? emptyList() : Arrays.asList(command.split(" "));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this work if args have spaces ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

full command might be problematic to parse properly. I'm thinking about windows in particular.

/ # cat Cmd.java public class Cmd { public static void main(String... args) { System.out.println(System.getProperty("sun.java.command")); } } / # java Cmd.java "foo bar" jdk.compiler/com.sun.tools.javac.launcher.Main Cmd.java foo bar 

...

/ # java -jar /tmp/foo\ bar/cmd.jar "foo bar" /tmp/foo bar/cmd.jar foo bar 
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that would work well. But that is the current behavior:

public ProcessInfo() {
// Besides "sun.java.command" property is not an standard, all main JDKs has set this
// property.
// Tested on:
// - OracleJDK, OpenJDK, AdoptOpenJDK, IBM JDK, Azul Zulu JDK, Amazon Coretto JDK
final String command = System.getProperty("sun.java.command");
if (command == null || command.isEmpty()) {
return;
}
final String[] split = command.trim().split(" ");
if (split.length == 0 || split[0].isEmpty()) {
return;
}
final String candidate = split[0];
if (candidate.toLowerCase(Locale.ROOT).endsWith(".jar")) {
jarFile = new File(candidate);
} else {
mainClass = candidate;
}
}

So far, I would be in favor to keep it as is, document the limitation, and revisit it later when I will finally have time to do LP work (this dev is mostly based of R&D week and overnight time). WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes OK.

Eventually this can be a good call as a future improvement to leverage ProcessHandle when the JVM is at least Java 9.

}

private String getCommandName() {
return fullCommand.isEmpty() ? null : fullCommand.get(0);
}

private List<String> getCommandArguments() {
if (fullCommand.isEmpty()) {
return fullCommand;
} else {
return fullCommand.subList(1, fullCommand.size());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package datadog.environment;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
* Safely queries environment variables against security manager.
*
* @see <a href="https://docs.oracle.com/javase/8/docs/api/java/lang/SecurityManager.html">Security
* Manager</a>
*/
public final class EnvironmentVariables {
private EnvironmentVariables() {}

/**
* Gets an environment variable value.
*
* @param name The environment variable name.
* @return The environment variable value, {@code null} if missing or can't be retrieved.
*/
public static @Nullable String get(String name) {
return getOrDefault(name, null);
}

/**
* Gets an environment variable value, or default value if missing or can't be retrieved.
*
* @param name The environment variable name.
* @param defaultValue The default value to return if the environment variable is missing or can't
* be retrieved.
* @return The environment variable value, {@code defaultValue} if missing or can't be retrieved.
*/
public static String getOrDefault(@Nonnull String name, String defaultValue) {
try {
String value = System.getenv(name);
return value == null ? defaultValue : value;
} catch (SecurityException e) {
return defaultValue;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package datadog.environment;

import java.util.ArrayList;
import java.util.List;

/**
* This class represents a Java version according the String Naming Convention.
*
* @see <a href="https://www.oracle.com/java/technologies/javase/versioning-naming.html">String
* Naming Convention</a>
*/
final class JavaVersion {
final int major;
final int minor;
final int update;

JavaVersion(int major, int minor, int update) {
this.major = major;
this.minor = minor;
this.update = update;
}

static JavaVersion getRuntimeVersion() {
return parseJavaVersion(SystemProperties.getOrDefault("java.version", ""));
}

static JavaVersion parseJavaVersion(String javaVersion) {
// Remove pre-release part, usually -ea
final int indexOfDash = javaVersion.indexOf('-');
if (indexOfDash >= 0) {
javaVersion = javaVersion.substring(0, indexOfDash);
}

int major = 0;
int minor = 0;
int update = 0;

try {
List<Integer> nums = splitDigits(javaVersion);
major = nums.get(0);

// for java 1.6/1.7/1.8
if (major == 1) {
major = nums.get(1);
minor = nums.get(2);
update = nums.get(3);
} else {
minor = nums.get(1);
update = nums.get(2);
}
} catch (NumberFormatException | IndexOutOfBoundsException e) {
// unable to parse version string - do nothing
}
return new JavaVersion(major, minor, update);
}

/* The method splits java version string by digits. Delimiters are: dot, underscore and plus */
private static List<Integer> splitDigits(String str) {
List<Integer> results = new ArrayList<>();

int len = str.length();
int value = 0;
for (int i = 0; i < len; i++) {
char ch = str.charAt(i);
if (ch >= '0' && ch <= '9') {
value = value * 10 + (ch - '0');
} else if (ch == '.' || ch == '_' || ch == '+') {
results.add(value);
value = 0;
} else {
throw new NumberFormatException();
}
}
results.add(value);
return results;
}

public boolean is(int major) {
return this.major == major;
}

public boolean is(int major, int minor) {
return this.major == major && this.minor == minor;
}

public boolean is(int major, int minor, int update) {
return this.major == major && this.minor == minor && this.update == update;
}

public boolean isAtLeast(int major, int minor, int update) {
return isAtLeast(this.major, this.minor, this.update, major, minor, update);
}

public boolean isBetween(
int fromMajor, int fromMinor, int fromUpdate, int toMajor, int toMinor, int toUpdate) {
return isAtLeast(toMajor, toMinor, toUpdate, fromMajor, fromMinor, fromUpdate)
&& isAtLeast(fromMajor, fromMinor, fromUpdate)
&& !isAtLeast(toMajor, toMinor, toUpdate);
}

private static boolean isAtLeast(
int major, int minor, int update, int atLeastMajor, int atLeastMinor, int atLeastUpdate) {
return (major > atLeastMajor)
|| (major == atLeastMajor && minor > atLeastMinor)
|| (major == atLeastMajor && minor == atLeastMinor && update >= atLeastUpdate);
}
}
Loading
Loading