diff --git a/components/environment/build.gradle.kts b/components/environment/build.gradle.kts new file mode 100644 index 00000000000..e06feeb39fd --- /dev/null +++ b/components/environment/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `java-library` +} + +apply(from = "$rootDir/gradle/java.gradle") + +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 +} diff --git a/components/environment/src/main/java/datadog/environment/CommandLine.java b/components/environment/src/main/java/datadog/environment/CommandLine.java new file mode 100644 index 00000000000..669f2fb94f1 --- /dev/null +++ b/components/environment/src/main/java/datadog/environment/CommandLine.java @@ -0,0 +1,45 @@ +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: + * + * + */ +class CommandLine { + private static final String SUN_JAVA_COMMAND_PROPERTY = "sun.java.command"; + final List fullCommand = findFullCommand(); + final String command = getCommand(); + final List arguments = getCommandArguments(); + + @SuppressForbidden // split on single-character uses fast path + private List findFullCommand() { + String command = SystemProperties.getOrDefault(SUN_JAVA_COMMAND_PROPERTY, "").trim(); + return command.isEmpty() ? emptyList() : Arrays.asList(command.split(" ")); + } + + private String getCommand() { + return fullCommand.isEmpty() ? null : fullCommand.get(0); + } + + private List getCommandArguments() { + if (fullCommand.isEmpty()) { + return fullCommand; + } else { + return fullCommand.subList(1, fullCommand.size()); + } + } +} diff --git a/components/environment/src/main/java/datadog/environment/EnvironmentVariables.java b/components/environment/src/main/java/datadog/environment/EnvironmentVariables.java new file mode 100644 index 00000000000..96f60bd6c4e --- /dev/null +++ b/components/environment/src/main/java/datadog/environment/EnvironmentVariables.java @@ -0,0 +1,41 @@ +package datadog.environment; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Safely queries environment variables against security manager. + * + * @see Security + * Manager + */ +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; + } + } +} diff --git a/components/environment/src/main/java/datadog/environment/JavaVersion.java b/components/environment/src/main/java/datadog/environment/JavaVersion.java new file mode 100644 index 00000000000..48aaed0c66e --- /dev/null +++ b/components/environment/src/main/java/datadog/environment/JavaVersion.java @@ -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 String + * Naming Convention + */ +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 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 splitDigits(String str) { + List 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); + } +} diff --git a/components/environment/src/main/java/datadog/environment/JavaVirtualMachine.java b/components/environment/src/main/java/datadog/environment/JavaVirtualMachine.java new file mode 100644 index 00000000000..fe884a81eb0 --- /dev/null +++ b/components/environment/src/main/java/datadog/environment/JavaVirtualMachine.java @@ -0,0 +1,212 @@ +package datadog.environment; + +import java.util.List; +import java.util.Locale; +import javax.annotation.Nullable; + +public final class JavaVirtualMachine { + private static final CommandLine commandLine = new CommandLine(); + private static final JavaVersion javaVersion = JavaVersion.getRuntimeVersion(); + private static final Runtime runtime = new Runtime(); + + private JavaVirtualMachine() {} + + public static boolean isJavaVersion(int major) { + return javaVersion.is(major); + } + + public static boolean isJavaVersion(int major, int minor) { + return javaVersion.is(major, minor); + } + + public static boolean isJavaVersion(int major, int minor, int update) { + return javaVersion.is(major, minor, update); + } + + public static boolean isJavaVersionAtLeast(int major) { + return isJavaVersionAtLeast(major, 0, 0); + } + + public static boolean isJavaVersionAtLeast(int major, int minor) { + return isJavaVersionAtLeast(major, minor, 0); + } + + public static boolean isJavaVersionAtLeast(int major, int minor, int update) { + return javaVersion.isAtLeast(major, minor, update); + } + + /** + * Checks if the Java version is between {@code fromMajor} (inclusive) and {@code toMajor} + * (exclusive). + * + * @param fromMajor major from version (inclusive) + * @param toMajor major to version (exclusive) + * @return if the current java version is between the from version (inclusive) and the to version + * exclusive + */ + public static boolean isJavaVersionBetween(int fromMajor, int toMajor) { + return isJavaVersionBetween(fromMajor, 0, toMajor, 0); + } + + /** + * Checks if the Java version is between {@code fromMajor.fromMinor} (inclusive) and {@code + * toMajor.toMinor} (exclusive). + * + * @param fromMajor major from version (inclusive) + * @param fromMinor minor from version (inclusive) + * @param toMajor major to version (exclusive) + * @param toMinor minor to version (exclusive) + * @return if the current java version is between the from version (inclusive) and the to version + * exclusive + */ + public static boolean isJavaVersionBetween( + int fromMajor, int fromMinor, int toMajor, int toMinor) { + return isJavaVersionBetween(fromMajor, fromMinor, 0, toMajor, toMinor, 0); + } + + /** + * Checks if the Java version is between {@code fromMajor.fromMinor.fromUpdate} (inclusive) and + * {@code toMajor.toMinor.toUpdate} (exclusive). + * + * @param fromMajor major from version (inclusive) + * @param fromMinor minor from version (inclusive) + * @param fromUpdate update from version (inclusive) + * @param toMajor major to version (exclusive) + * @param toMinor minor to version (exclusive) + * @param toUpdate update to version (exclusive) + * @return if the current java version is between the from version (inclusive) and the to version + * exclusive + */ + public static boolean isJavaVersionBetween( + int fromMajor, int fromMinor, int fromUpdate, int toMajor, int toMinor, int toUpdate) { + return javaVersion.isBetween(fromMajor, fromMinor, fromUpdate, toMajor, toMinor, toUpdate); + } + + /** + * Checks whether the current JVM is an Oracle JDK 8. + * + * @return {@code true} if the current JVM is an Oracle JDK 8, {@code false} otherwise. + */ + public static boolean isOracleJDK8() { + return isJavaVersion(8) + && runtime.vendor.contains("Oracle") + && !runtime.name.contains("OpenJDK"); + } + + public static boolean isJ9() { + return SystemProperties.getOrDefault("java.vm.name", "").contains("J9"); + } + + public static boolean isIbm8() { + return isJavaVersion(8) && runtime.vendor.contains("IBM"); + } + + public static boolean isGraalVM() { + return runtime.vendorVersion.toLowerCase().contains("graalvm"); + } + + public static String getLangVersion() { + return String.valueOf(javaVersion.major); + } + + public static String getRuntimeVendor() { + return runtime.vendor; + } + + public static String getRuntimeVersion() { + return runtime.version; + } + + public static String getRuntimePatches() { + return runtime.patches; + } + + /** + * Gets the JVM options. + * + * @return The JVM options, an empty collection if they can't be retrieved. + */ + public static List getVmOptions() { + return JvmOptionsHolder.JVM_OPTIONS.VM_OPTIONS; + } + + /** + * Gets the command arguments. + * + * @return The command arguments, an empty collection if missing or can't be retrieved. + */ + public static List getCommandArguments() { + return commandLine.arguments; + } + + /** + * Gets the JVM runtime main class name. + * + * @return The JVM runtime main class name, {@code null} if using JAR file instead or can't be + * retrieved. + */ + public static @Nullable String getMainClass() { + return commandLine.command != null && !isJarName(commandLine.command) + ? commandLine.command + : null; + } + + /** + * Gets the JVM runtime jar file. + * + * @return The JVM runtime jar file, {@code null} if using main class instead or can't be + * retrieved. + */ + public static @Nullable String getJarFile() { + return commandLine.command != null && isJarName(commandLine.command) + ? commandLine.command + : null; + } + + private static boolean isJarName(String argument) { + return argument.toLowerCase(Locale.ROOT).endsWith(".jar"); + } + + private static class JvmOptionsHolder { + private static final JvmOptions JVM_OPTIONS = new JvmOptions(); + } + + static final class Runtime { + /* + * Example: + * jvm -> "AdoptOpenJDK 1.8.0_265-b01" + * + * name -> "OpenJDK" + * vendor -> "AdoptOpenJDK" + * version -> "1.8.0_265" + * patches -> "b01" + */ + public final String name; + + public final String vendor; + public final String version; + public final String vendorVersion; + public final String patches; + + public Runtime() { + this( + SystemProperties.get("java.version"), + SystemProperties.get("java.runtime.version"), + SystemProperties.get("java.runtime.name"), + SystemProperties.get("java.vm.vendor"), + SystemProperties.get("java.vendor.version")); + } + + // Only visible for testing + Runtime(String javaVer, String rtVer, String name, String vendor, String vendorVersion) { + this.name = name == null ? "" : name; + this.vendor = vendor == null ? "" : vendor; + javaVer = javaVer == null ? "" : javaVer; + this.version = javaVer; + this.vendorVersion = vendorVersion == null ? "" : vendorVersion; + rtVer = javaVer.isEmpty() || rtVer == null ? javaVer : rtVer; + int patchStart = javaVer.length() + 1; + this.patches = (patchStart >= rtVer.length()) ? "" : rtVer.substring(javaVer.length() + 1); + } + } +} diff --git a/components/environment/src/main/java/datadog/environment/JvmOptions.java b/components/environment/src/main/java/datadog/environment/JvmOptions.java new file mode 100644 index 00000000000..90250d4b323 --- /dev/null +++ b/components/environment/src/main/java/datadog/environment/JvmOptions.java @@ -0,0 +1,211 @@ +package datadog.environment; + +import static datadog.environment.OperatingSystem.isLinux; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +import de.thetaphi.forbiddenapis.SuppressForbidden; +import java.io.BufferedReader; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ListIterator; +import java.util.StringTokenizer; + +/** Fetches and captures the JVM options. */ +class JvmOptions { + final String[] PROCFS_CMDLINE = readProcFsCmdLine(); + final List VM_OPTIONS = findVmOptions(); + + // static { + // RuntimeException runtimeException = new RuntimeException(); + // runtimeException.printStackTrace(System.out); + // } + + @SuppressForbidden // split on single-character uses fast path + private String[] readProcFsCmdLine() { + if (!isLinux()) { + try { + Path cmdlinePath = Paths.get("/proc/self/cmdline"); + if (Files.exists(cmdlinePath) && Files.isReadable(cmdlinePath)) { + try (BufferedReader in = Files.newBufferedReader(cmdlinePath)) { + return in.readLine().split("\0"); + } + } + } catch (Throwable ignored) { + } + } + return null; + } + + @SuppressForbidden // Class.forName() as backup + private List findVmOptions() { + // Try ProcFS on Linux + if (PROCFS_CMDLINE != null) { + // Start at 1 to skip "java" command itself + int index = 1; + // Look for main class or "-jar", end of VM options + for (; index < PROCFS_CMDLINE.length; index++) { + if (!PROCFS_CMDLINE[index].startsWith("-") || "-jar".equals(PROCFS_CMDLINE[index])) { + break; + } + } + // Create list of VM options + String[] vmOptionsArray = new String[index - 1]; + System.arraycopy(PROCFS_CMDLINE, 1, vmOptionsArray, 0, vmOptionsArray.length); + List vmOptions = Arrays.asList(vmOptionsArray); + // Substitute @argfile by their content + ListIterator iterator = vmOptions.listIterator(); + while (iterator.hasNext()) { + String vmOption = iterator.next(); + if (vmOption.startsWith("@")) { + iterator.remove(); + for (String argument : getArgumentsFromFile(vmOption)) { + iterator.add(argument); + } + } + } + // Insert JAVA_TOOL_OPTIONS at the start if present + List toolOptions = getToolOptions(); + if (!toolOptions.isEmpty()) { + vmOptions.addAll(0, toolOptions); + } + return vmOptions; + } + + // Try Oracle-based + // IBM Semeru Runtime 1.8.0_345-b01 will throw UnsatisfiedLinkError here. + try { + final Class managementFactoryHelperClass = + Class.forName("sun.management.ManagementFactoryHelper"); + final Class vmManagementClass = Class.forName("sun.management.VMManagement"); + + Object vmManagement; + try { + vmManagement = + managementFactoryHelperClass.getDeclaredMethod("getVMManagement").invoke(null); + } catch (final NoSuchMethodException e) { + // Older vm before getVMManagement() existed + final Field field = managementFactoryHelperClass.getDeclaredField("jvm"); + field.setAccessible(true); + vmManagement = field.get(null); + field.setAccessible(false); + } + //noinspection unchecked + return (List) vmManagementClass.getMethod("getVmArguments").invoke(vmManagement); + } catch (final ReflectiveOperationException | UnsatisfiedLinkError ignored) { + // Ignored exception + } + + // Try IBM-based. + try { + final Class VMClass = Class.forName("com.ibm.oti.vm.VM"); + final String[] argArray = (String[]) VMClass.getMethod("getVMArgs").invoke(null); + return Arrays.asList(argArray); + } catch (final ReflectiveOperationException ignored) { + // Ignored exception + } + + // Fallback to default + try { + return ManagementFactory.getRuntimeMXBean().getInputArguments(); + } catch (final Throwable t) { + // Throws InvocationTargetException on modularized applications + // with non-opened java.management module + System.err.println("WARNING: Unable to get VM args using managed beans"); + } + return emptyList(); + } + + @SuppressForbidden // split on single-character uses fast path + private List findFullCommand() { + // 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 + String command = SystemProperties.getOrDefault("sun.java.command", "").trim(); + return command.isEmpty() ? emptyList() : Arrays.asList(command.split(" ")); + } + + private static List getArgumentsFromFile(String argFile) { + String filename = argFile.substring(1); + Path path = Paths.get(filename); + if (!Files.exists(path) || !Files.isReadable(path)) { + return singletonList(argFile); + } + List args = new ArrayList<>(); + try { + for (String line : Files.readAllLines(path)) { + // Use default delimiters that matches argfiles separator specification + StringTokenizer tokenizer = new StringTokenizer(line); + while (tokenizer.hasMoreTokens()) { + args.add(tokenizer.nextToken()); + } + } + return args; + } catch (IOException e) { + return singletonList(argFile); + } + } + + private static List getToolOptions() { + String javaToolOptions = EnvironmentVariables.getOrDefault("JAVA_TOOL_OPTIONS", ""); + return javaToolOptions.isEmpty() ? emptyList() : parseToolOptions(javaToolOptions); + } + + /** + * Parse the JAVA_TOOL_OPTIONS environment variable according the JVMTI specifications + * + * @param javaToolOptions The JAVA_TOOL_OPTIONS environment variable. + * @return The parsed JVM options. + * @see JVMTI + * specifications + */ + static List parseToolOptions(String javaToolOptions) { + List options = new ArrayList<>(); + StringBuilder option = new StringBuilder(); + boolean inQuotes = false; + char quoteChar = 0; + + for (int i = 0; i < javaToolOptions.length(); i++) { + char c = javaToolOptions.charAt(i); + if (inQuotes) { + if (quoteChar == c) { + inQuotes = false; + } else { + option.append(c); + } + } else if (c == '"' || c == '\'') { + inQuotes = true; + quoteChar = c; + } else if (Character.isWhitespace(c)) { + if (option.length() > 0) { + options.add(option.toString()); + option.setLength(0); + } + } else { + option.append(c); + } + } + if (option.length() > 0) { + options.add(option.toString()); + } + return options; + } + + private static List split(String str, String delimiter) { + List parts = new ArrayList<>(); + StringTokenizer tokenizer = new StringTokenizer(str, delimiter); + while (tokenizer.hasMoreTokens()) { + parts.add(tokenizer.nextToken()); + } + return parts; + } +} diff --git a/components/environment/src/main/java/datadog/environment/OperatingSystem.java b/components/environment/src/main/java/datadog/environment/OperatingSystem.java new file mode 100644 index 00000000000..06c72bad553 --- /dev/null +++ b/components/environment/src/main/java/datadog/environment/OperatingSystem.java @@ -0,0 +1,149 @@ +package datadog.environment; + +import static java.util.Locale.ROOT; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +/** Detects operating systems and libc library. */ +public final class OperatingSystem { + private static final String OS_NAME_PROPERTY = "os.name"; + private static final String OS_ARCH_PROPERTY = "os.arch"; + + private OperatingSystem() {} + + /** + * Checks whether the operating system is Linux based. + * + * @return @{@code true} if operating system is Linux based, {@code false} otherwise. + */ + public static boolean isLinux() { + return propertyContains(OS_NAME_PROPERTY, "linux"); + } + + /** + * Checks whether the operating system is Windows. + * + * @return @{@code true} if operating system is Windows, {@code false} otherwise. + */ + public static boolean isWindows() { + // https://mkyong.com/java/how-to-detect-os-in-java-systemgetpropertyosname/ + return propertyContains(OS_NAME_PROPERTY, "win"); + } + + /** + * Checks whether the operating system is macOS. + * + * @return @{@code true} if operating system is macOS, {@code false} otherwise. + */ + public static boolean isMacOs() { + return propertyContains(OS_NAME_PROPERTY, "mac"); + } + + /** + * Checks whether the architecture is AArch64. + * + * @return {@code true} if the architecture is AArch64, {@code false} otherwise. + */ + public static boolean isAarch64() { + return propertyContains(OS_ARCH_PROPERTY, "aarch64"); + } + + private static boolean propertyContains(String property, String content) { + return SystemProperties.getOrDefault(property, "").toLowerCase(ROOT).contains(content); + } + + /** + * Checks whether the libc is MUSL. + * + * @return {@code true} if the libc is MUSL, {@code false} otherwise. + */ + public static boolean isMusl() { + if (!isLinux()) { + return false; + } + // check the Java exe then fall back to proc/self maps + try { + return isMuslJavaExecutable(); + } catch (IOException e) { + try { + return isMuslProcSelfMaps(); + } catch (IOException ignore) { + return false; + } + } + } + + private static boolean isMuslProcSelfMaps() throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader("/proc/self/maps"))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains("-musl-")) { + return true; + } + if (line.contains("/libc.")) { + return false; + } + } + } + return false; + } + + /** + * There is information about the linking in the ELF file. Since properly parsing ELF is not + * trivial this code will attempt a brute-force approach and will scan the first 4096 bytes of the + * 'java' program image for anything prefixed with `/ld-` - in practice this will contain + * `/ld-musl` for musl systems and probably something else for non-musl systems (e.g. + * `/ld-linux-...`). However, if such string is missing should indicate that the system is not a + * musl one. + */ + private static boolean isMuslJavaExecutable() throws IOException { + byte[] magic = new byte[] {(byte) 0x7f, (byte) 'E', (byte) 'L', (byte) 'F'}; + byte[] prefix = new byte[] {(byte) '/', (byte) 'l', (byte) 'd', (byte) '-'}; // '/ld-*' + byte[] musl = new byte[] {(byte) 'm', (byte) 'u', (byte) 's', (byte) 'l'}; // 'musl' + + Path binary = Paths.get(SystemProperties.getOrDefault("java.home", ""), "bin", "java"); + byte[] buffer = new byte[4096]; + + try (InputStream is = Files.newInputStream(binary)) { + int read = is.read(buffer, 0, 4); + if (read != 4 || !containsArray(buffer, 0, magic)) { + throw new IOException(Arrays.toString(buffer)); + } + read = is.read(buffer); + if (read <= 0) { + throw new IOException(); + } + int prefixPos = 0; + for (int i = 0; i < read; i++) { + if (buffer[i] == prefix[prefixPos]) { + if (++prefixPos == prefix.length) { + return containsArray(buffer, i + 1, musl); + } + } else { + prefixPos = 0; + } + } + } + return false; + } + + private static boolean containsArray(byte[] container, int offset, byte[] contained) { + for (int i = 0; i < contained.length; i++) { + int leftPos = offset + i; + if (leftPos >= container.length) { + return false; + } + if (container[leftPos] != contained[i]) { + return false; + } + } + return true; + } +} diff --git a/components/environment/src/main/java/datadog/environment/SystemProperties.java b/components/environment/src/main/java/datadog/environment/SystemProperties.java new file mode 100644 index 00000000000..29b8e25804c --- /dev/null +++ b/components/environment/src/main/java/datadog/environment/SystemProperties.java @@ -0,0 +1,53 @@ +package datadog.environment; + +/** + * Safely queries system properties against security manager. + * + * @see Security + * Manager + */ +public final class SystemProperties { + private SystemProperties() {} + + /** + * Gets a system property value. + * + * @param property The system property name. + * @return The system property value, {@code null} if missing or can't be retrieved. + */ + public static String get(String property) { + return getOrDefault(property, null); + } + + /** + * Gets a system property value, or default value if missing or can't be retrieved. + * + * @param property The system property name. + * @param defaultValue The default value to return if the system property is missing or can't be + * retrieved. + * @return The system property value, {@code defaultValue} if missing or can't be retrieved. + */ + public static String getOrDefault(String property, String defaultValue) { + try { + return System.getProperty(property, defaultValue); + } catch (SecurityException ignored) { + return defaultValue; + } + } + + /** + * Sets a system property value. + * + * @param property The system property name. + * @param value The system property value to set. + * @return {@code true} if the system property was successfully set, {@code} false otherwise. + */ + public static boolean set(String property, String value) { + try { + System.setProperty(property, value); + return true; + } catch (SecurityException ignored) { + return false; + } + } +} diff --git a/components/environment/src/test/java/datadog/environment/CommandLineTest.java b/components/environment/src/test/java/datadog/environment/CommandLineTest.java new file mode 100644 index 00000000000..c362a02d86c --- /dev/null +++ b/components/environment/src/test/java/datadog/environment/CommandLineTest.java @@ -0,0 +1,266 @@ +package datadog.environment; + +import static datadog.environment.CommandLineTest.RunArguments.of; +import static java.io.File.separator; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class CommandLineTest { + private static final String JVM_OPTIONS_MARKER = "-- JVM OPTIONS --"; + private static final String CMD_ARGUMENTS_MARKER = "-- CMD ARGUMENTS --"; + private static final String REAL_CMD_ARGUMENTS_MARKER = "-- REAL CMD ARGUMENTS --"; + + static Stream data() { + // spotless:off + return Stream.of( + arguments( + "No JVM options nor command argument", + of(emptyList(), emptyList()), + of(emptyList(), emptyList()) + ), + arguments( + "JVM options only", + of(asList("-Xmx128m", "-XX:+UseG1GC", "-Dtest.property=value"), emptyList()), + of(asList("-Xmx128m", "-XX:+UseG1GC", "-Dtest.property=value"), emptyList())), + arguments( + "Command arguments only", + of(emptyList(), asList("arg1", "arg2")), + of(emptyList(), asList("arg1", "arg2"))), + arguments( + "Both JVM options and command arguments", + of(asList("-Xmx128m", "-XX:+UseG1GC", "-Dtest.property=value"), asList("arg1", "arg2")), + of(asList("-Xmx128m", "-XX:+UseG1GC", "-Dtest.property=value"), asList("arg1", "arg2"))), + arguments( + "JVM options from argfile", + of(asList("-Dtest.property=value", argFile("space-separated")), asList("arg1", "arg2")), + of(flatten("-Dtest.property=value", expectedArsFromArgFile("space-separated")), asList("arg1", "arg2"))) + ); + // spotless:on + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("data") + void testGetVmArguments(String useCase, RunArguments arguments, RunArguments expectedArguments) + throws Exception { + if (useCase.contains("argfile")) { + System.out.println(">>> java.home" + System.getProperty("java.home")); + System.err.println(">>> java.home" + System.getProperty("java.home")); + } + // Skip unsupported test cases + skipArgFileTestOnJava8(arguments); + // keepDisabledArgFileOnLinuxOnly(arguments); + // Run test process + Result result = forkAndRunWithArgs(CommandLineTestProcess.class, arguments); + // Check results + assertEquals(expectedArguments.jvmOptions, result.jvmOptions, "Failed to get JVM options"); + assertEquals(result.realCmdArgs, result.cmdArgs, "Failed to get command arguments"); + assertEquals(result.realCmdArgs, expectedArguments.cmdArgs, "Unexpected command arguments"); + } + + // @Test + // // Disable the test for Java 8. Using -PtestJvm will set Java HOME to the JVM to use to run + // this + // // test. + // @DisabledIfSystemProperty(named = "java.home", matches = ".*[-/]8\\..*") + // public void testGetVmArgumentsFromArgFile() throws Exception { + // List jvmOptions = asList("-Dproperty1=value1", argFile("space-separated")); + // List expectedJvmOptions = flatten("-Dproperty1=value1", + // expectedArsFromArgFile("space-separated")); + // Result result = forkAndRunWithArgs(CommandLineTestProcess.class, of(jvmOptions, + // emptyList())); + // assertEquals(expectedJvmOptions, result.jvmOptions, "Failed to get JVM options"); + // // TODO CMD ARGS + // } + // + // @Test + // // Enable only for Java 20+: https://bugs.openjdk.org/browse/JDK-8297258 + // // Using -PtestJvm will set Java HOME to the JVM to use to run this test. + // @EnabledIfSystemProperty(named = "java.home", matches = ".*[-/]21\\..*") + // public void testGetVmArgumentsFromDisabledArgFile() throws Exception { + // List jvmArgs = asList("-Dproperty1=value1", "--disable-@files"); + // List cmdArgs = asList("arg1", argFile("space-separated"), "arg2"); + // // --disable-@files won't be reported + // List expectedJvmOptions = singletonList("-Dproperty1=value1"); + // List expectedCmdArgs = flatten(CommandLineTestProcess.class.getName(), cmdArgs); + // Result result = forkAndRunWithArgs(CommandLineTestProcess.class, of(jvmArgs, cmdArgs)); + // assertEquals(expectedJvmOptions, result.jvmOptions, "Failed to get JVM options"); + // assertEquals(expectedCmdArgs, result.cmdArgs, "Failed to get command arguments"); + // } + + private static void skipArgFileTestOnJava8(RunArguments arguments) { + boolean useArgFile = false; + for (String jvmOption : arguments.jvmOptions) { + if (jvmOption.startsWith("@")) { + useArgFile = true; + break; + } + } + if (!useArgFile) { + for (String cmdArg : arguments.cmdArgs) { + if (cmdArg.startsWith("@")) { + useArgFile = true; + break; + } + } + } + if (useArgFile) { + assumeFalse(System.getProperty("java.home").matches(".*[-/]8[./].*")); + } + } + + private static void keepDisabledArgFileOnLinuxOnly(RunArguments arguments) { + boolean disableArgFile = false; + for (String jvmOptions : arguments.jvmOptions) { + if (jvmOptions.startsWith("--disable-@files")) { + disableArgFile = true; + break; + } + } + if (disableArgFile) { + assumeTrue(OperatingSystem.isLinux()); + } + } + + private static String argFile(String name) { + return "@src/test/resources/argfiles/" + name + ".txt"; + } + + private static List expectedArsFromArgFile(String name) { + List arguments = new ArrayList<>(); + try (InputStream stream = + requireNonNull( + CommandLineTest.class.getResourceAsStream("/argfiles/" + name + "-expected.txt")); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + String line; + while ((line = reader.readLine()) != null) { + arguments.add(line); + } + } catch (IOException e) { + Assertions.fail("Failed to read expected args from " + name + "argfile", e); + } + return arguments; + } + + private Result forkAndRunWithArgs(Class clazz, RunArguments arguments) + throws IOException, InterruptedException { + // Build the command to run a new Java process + List command = new ArrayList<>(); + command.add(System.getProperty("java.home") + separator + "bin" + separator + "java"); + command.addAll(arguments.jvmOptions); + command.add("-cp"); + command.add(System.getProperty("java.class.path")); + command.add(clazz.getName()); + command.addAll(arguments.cmdArgs); + // Start the process + ProcessBuilder processBuilder = new ProcessBuilder(command); + Process process = processBuilder.start(); + // Read and parse output and error streams + Result result = new Result(); + List current = null; + String output = ""; + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + if (JVM_OPTIONS_MARKER.equals(line)) { + current = result.jvmOptions; + } else if (CMD_ARGUMENTS_MARKER.equals(line)) { + current = result.cmdArgs; + } else if (REAL_CMD_ARGUMENTS_MARKER.equals(line)) { + current = result.realCmdArgs; + } else if (current != null) { + current.add(line); + } + output += line + "\n"; + } + } + String error = ""; + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String line; + while ((line = reader.readLine()) != null) { + error += line + "\n"; + } + } + // Wait for the process to complete + int exitCode = process.waitFor(); + // Dumping state on error + if (exitCode != 0) { + System.err.println("Error running command: " + String.join(" ", command)); + System.err.println("Exit code " + exitCode + " with output:"); + System.err.println(output); + System.err.println("and error:"); + System.err.println(error); + } + assertEquals(0, exitCode, "Process should exit normally"); + return result; + } + + private static List flatten(Object... values) { + List result = new ArrayList<>(); + for (Object value : values) { + if (value instanceof Collection) { + result.addAll((Collection) value); + } else { + result.add(value.toString()); + } + } + return result; + } + + static class RunArguments { + List jvmOptions = new ArrayList<>(); + List cmdArgs = new ArrayList<>(); + + static RunArguments of(List jvmArgs, List cmdArgs) { + RunArguments arguments = new RunArguments(); + arguments.jvmOptions = jvmArgs; + arguments.cmdArgs = cmdArgs; + return arguments; + } + } + + static class Result extends CommandLineTest.RunArguments { + List realCmdArgs = new ArrayList<>(); + String command; // TODO + } + + // This class will be executed in the subprocess + public static class CommandLineTestProcess { + public static void main(String[] args) { + // Print each VM argument on a new line + System.out.println(JVM_OPTIONS_MARKER); + for (String option : JavaVirtualMachine.getVmOptions()) { + System.out.println(option); + } + // Print each command argument on a new line + System.out.println(CMD_ARGUMENTS_MARKER); + for (String arg : JavaVirtualMachine.getCommandArguments()) { + System.out.println(arg); + } + // Print each real command argument on a new line + System.out.println(REAL_CMD_ARGUMENTS_MARKER); + for (String arg : args) { + System.out.println(arg); + } + } + } +} diff --git a/components/environment/src/test/java/datadog/environment/EnvironmentVariablesTest.java b/components/environment/src/test/java/datadog/environment/EnvironmentVariablesTest.java new file mode 100644 index 00000000000..80cb4f601b6 --- /dev/null +++ b/components/environment/src/test/java/datadog/environment/EnvironmentVariablesTest.java @@ -0,0 +1,27 @@ +package datadog.environment; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class EnvironmentVariablesTest { + private static final String EXISTING_ENV_VAR = "JAVA_8_HOME"; + private static final String MISSING_ENV_VAR = "UNDEFINED_ENV_VAR"; + + @Test + void testGet() { + assertNotNull(EnvironmentVariables.get(EXISTING_ENV_VAR)); + assertNull(EnvironmentVariables.get(MISSING_ENV_VAR)); + assertThrows(NullPointerException.class, () -> EnvironmentVariables.get(null)); + } + + @Test + void testGetOrDefault() { + assertNotNull(EnvironmentVariables.getOrDefault(EXISTING_ENV_VAR, null)); + + assertEquals("", EnvironmentVariables.getOrDefault(MISSING_ENV_VAR, "")); + assertNull(EnvironmentVariables.getOrDefault(MISSING_ENV_VAR, null)); + + assertThrows(NullPointerException.class, () -> EnvironmentVariables.getOrDefault(null, "")); + } +} diff --git a/components/environment/src/test/java/datadog/environment/JavaVersionTest.java b/components/environment/src/test/java/datadog/environment/JavaVersionTest.java new file mode 100644 index 00000000000..9fcb3fc3a13 --- /dev/null +++ b/components/environment/src/test/java/datadog/environment/JavaVersionTest.java @@ -0,0 +1,103 @@ +package datadog.environment; + +import static java.lang.Integer.MAX_VALUE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.of; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class JavaVersionTest { + static Stream argumentsParsing() { + return Stream.of( + of("", 0, 0, 0), + of("a.0.0", 0, 0, 0), + of("0.a.0", 0, 0, 0), + of("0.0.a", 0, 0, 0), + of("1.a.0_0", 0, 0, 0), + of("1.8.a_0", 0, 0, 0), + of("1.8.0_a", 0, 0, 0), + of("1.7", 7, 0, 0), + of("1.7.0", 7, 0, 0), + of("1.7.0_221", 7, 0, 221), + of("1.8", 8, 0, 0), + of("1.8.0", 8, 0, 0), + of("1.8.0_212", 8, 0, 212), + of("1.8.0_292", 8, 0, 292), + of("9-ea", 9, 0, 0), + of("9.0.4", 9, 0, 4), + of("9.1.2", 9, 1, 2), + of("10.0.2", 10, 0, 2), + of("11", 11, 0, 0), + of("11.0.6", 11, 0, 6), + of("11.0.11", 11, 0, 11), + of("12.0.2", 12, 0, 2), + of("13.0.2", 13, 0, 2), + of("14", 14, 0, 0), + of("14.0.2", 14, 0, 2), + of("15", 15, 0, 0), + of("15.0.2", 15, 0, 2), + of("16.0.1", 16, 0, 1), + of("11.0.9.1+1", 11, 0, 9), + of("11.0.6+10", 11, 0, 6), + of("17.0.4-x", 17, 0, 4)); + } + + @ParameterizedTest(name = "[{index}] {0} parsed as {1}.{2}.{3}") + @MethodSource("argumentsParsing") + void testParsing(String version, int major, int minor, int update) { + JavaVersion javaVersion = JavaVersion.parseJavaVersion(version); + + assertEquals(major, javaVersion.major); + assertEquals(minor, javaVersion.minor); + assertEquals(update, javaVersion.update); + + assertFalse(javaVersion.is(major - 1)); + assertTrue(javaVersion.is(major)); + assertFalse(javaVersion.is(major + 1)); + + assertFalse(javaVersion.is(major, minor - 1)); + assertTrue(javaVersion.is(major, minor)); + assertFalse(javaVersion.is(major, minor + 1)); + + assertFalse(javaVersion.is(major, minor, update - 1)); + assertTrue(javaVersion.is(major, minor, update)); + assertFalse(javaVersion.is(major, minor, update + 1)); + + assertTrue(javaVersion.isAtLeast(major, minor, update)); + + assertFalse(javaVersion.isBetween(major, minor, update, major, minor, update)); + + assertTrue(javaVersion.isBetween(major, minor, update, MAX_VALUE, MAX_VALUE, MAX_VALUE)); + assertTrue(javaVersion.isBetween(major, minor, update, major + 1, 0, 0)); + assertTrue(javaVersion.isBetween(major, minor, update, major, minor + 1, 0)); + assertTrue(javaVersion.isBetween(major, minor, update, major, minor, update + 1)); + + assertFalse(javaVersion.isBetween(major, minor, update, major - 1, 0, 0)); + assertFalse(javaVersion.isBetween(major, minor, update, major, minor - 1, 0)); + assertFalse(javaVersion.isBetween(major, minor, update, major, minor, update - 1)); + } + + static Stream argumentsAtLeast() { + return Stream.of( + of("17.0.5+8", 17, 0, 5), + of("17.0.5", 17, 0, 5), + of("17.0.6+8", 17, 0, 5), + of("11.0.17+8", 11, 0, 17), + of("11.0.18+8", 11, 0, 17), + of("11.0.17", 11, 0, 17), + of("1.8.0_352", 8, 0, 352), + of("1.8.0_362", 8, 0, 352)); + } + + @ParameterizedTest(name = "[{index}] {0} is at least {1}.{2}.{3}") + @MethodSource("argumentsAtLeast") + void testIsAtLeast(String version, int major, int minor, int update) { + JavaVersion javaVersion = JavaVersion.parseJavaVersion(version); + assertTrue(javaVersion.isAtLeast(major, minor, update)); + } +} diff --git a/components/environment/src/test/java/datadog/environment/JavaVirtualMachineTest.java b/components/environment/src/test/java/datadog/environment/JavaVirtualMachineTest.java new file mode 100644 index 00000000000..62b0ad3307f --- /dev/null +++ b/components/environment/src/test/java/datadog/environment/JavaVirtualMachineTest.java @@ -0,0 +1,169 @@ +package datadog.environment; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.condition.JRE.JAVA_11; +import static org.junit.jupiter.api.condition.JRE.JAVA_17; +import static org.junit.jupiter.api.condition.JRE.JAVA_21; +import static org.junit.jupiter.api.condition.JRE.JAVA_8; +import static org.junit.jupiter.api.condition.JRE.JAVA_9; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfSystemProperty; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class JavaVirtualMachineTest { + @Test + @EnabledOnJre(JAVA_8) + void onJava8Only() { + assertTrue(JavaVirtualMachine.isJavaVersion(8)); + assertFalse(JavaVirtualMachine.isJavaVersion(11)); + } + + @Test + @EnabledOnJre(JAVA_11) + void onJava11Only() { + assertFalse(JavaVirtualMachine.isJavaVersion(8)); + assertTrue(JavaVirtualMachine.isJavaVersion(11)); + assertFalse(JavaVirtualMachine.isJavaVersion(17)); + } + + @Test + @EnabledOnJre(JAVA_17) + void onJava17Only() { + assertFalse(JavaVirtualMachine.isJavaVersion(11)); + assertTrue(JavaVirtualMachine.isJavaVersion(17)); + assertFalse(JavaVirtualMachine.isJavaVersion(21)); + } + + @Test + @EnabledOnJre(JAVA_21) + void onJava21Only() { + assertFalse(JavaVirtualMachine.isJavaVersion(17)); + assertTrue(JavaVirtualMachine.isJavaVersion(21)); + assertFalse(JavaVirtualMachine.isJavaVersion(25)); + } + + @Test + void onJava7andHigher() { + assertTrue(JavaVirtualMachine.isJavaVersionAtLeast(7)); + } + + @Test + void onJava8AndHigher() { + for (int version = 7; version <= 8; version++) { + assertTrue(JavaVirtualMachine.isJavaVersionAtLeast(version)); + } + } + + @Test + @EnabledForJreRange(min = JAVA_11) + void onJava11AndHigher() { + for (int version = 7; version <= 11; version++) { + assertTrue(JavaVirtualMachine.isJavaVersionAtLeast(version)); + } + } + + @Test + @EnabledForJreRange(min = JAVA_17) + void onJava17AndHigher() { + for (int version = 7; version <= 17; version++) { + assertTrue(JavaVirtualMachine.isJavaVersionAtLeast(version)); + } + } + + @Test + @EnabledForJreRange(min = JAVA_21) + void onJava21AndHigher() { + for (int version = 7; version <= 21; version++) { + assertTrue(JavaVirtualMachine.isJavaVersionAtLeast(version)); + } + } + + @Test + @EnabledForJreRange(min = JAVA_8, max = JAVA_9) + void fromJava8to9() { + assertFalse(JavaVirtualMachine.isJavaVersionBetween(7, 8)); + assertTrue(JavaVirtualMachine.isJavaVersionBetween(8, 10)); + assertFalse(JavaVirtualMachine.isJavaVersionBetween(10, 11)); + } + + @Test + @EnabledForJreRange(min = JAVA_11, max = JAVA_11) + void fromJava11to17() { + assertFalse(JavaVirtualMachine.isJavaVersionBetween(8, 11)); + assertTrue(JavaVirtualMachine.isJavaVersionBetween(11, 18)); + assertFalse(JavaVirtualMachine.isJavaVersionBetween(18, 21)); + } + + @Test + @EnabledIfSystemProperty(named = "java.vendor.version", matches = ".*graalvm.*") + void onlyOnGraalVm() { + assertTrue(JavaVirtualMachine.isGraalVM()); + assertFalse(JavaVirtualMachine.isIbm8()); + assertFalse(JavaVirtualMachine.isJ9()); + assertFalse(JavaVirtualMachine.isOracleJDK8()); + } + + @Test + @EnabledIfSystemProperty(named = "java.vm.vendor", matches = ".*IBM.*") + @EnabledOnJre(JAVA_8) + void onlyOnIbm8() { + assertFalse(JavaVirtualMachine.isGraalVM()); + assertTrue(JavaVirtualMachine.isIbm8()); + assertFalse(JavaVirtualMachine.isJ9()); + assertFalse(JavaVirtualMachine.isOracleJDK8()); + } + + @Test + @EnabledIfSystemProperty(named = "java.vm.name", matches = ".*J9.*") + void onlyOnJ9() { + assertFalse(JavaVirtualMachine.isGraalVM()); + assertFalse(JavaVirtualMachine.isIbm8()); + assertTrue(JavaVirtualMachine.isJ9()); + assertFalse(JavaVirtualMachine.isOracleJDK8()); + } + + @Test + @EnabledIfSystemProperty(named = "java.vm.vendor", matches = ".*Oracle.*") + @DisabledIfSystemProperty(named = "java.runtime.name", matches = ".*OpenJDK.*") + @EnabledOnJre(JAVA_8) + void onlyOnOracleJDK8() { + assertFalse(JavaVirtualMachine.isGraalVM()); + assertFalse(JavaVirtualMachine.isIbm8()); + assertFalse(JavaVirtualMachine.isJ9()); + assertTrue(JavaVirtualMachine.isOracleJDK8()); + } + + @ParameterizedTest + @CsvSource( + value = { + "1.8.0_265 | 1.8.0_265-b01 | OpenJDK | AdoptOpenJDK | 1.8.0_265 | b01 | OpenJDK | AdoptOpenJDK", + "1.8.0_265 | 1.8-b01 | OpenJDK | AdoptOpenJDK | 1.8.0_265 | '' | OpenJDK | AdoptOpenJDK", + "19 | 19 | OpenJDK 64-Bit | Homebrew | 19 | '' | OpenJDK 64-Bit | Homebrew", + "17 | null | null | null | 17 | '' | '' | ''", + "null | 17 | null | null | '' | '' | '' | ''", + }, + nullValues = "null", + delimiter = '|') + void testRuntimeParsing( + String javaVersion, + String javaRuntimeVersion, + String javaRuntimeName, + String javaVmVendor, + String expectedVersion, + String expectedPatches, + String expectedName, + String expectedVendor) { + JavaVirtualMachine.Runtime runtime = + new JavaVirtualMachine.Runtime( + javaVersion, javaRuntimeVersion, javaRuntimeName, javaVmVendor, null); + assertEquals(expectedVersion, runtime.version); + assertEquals(expectedPatches, runtime.patches); + assertEquals(expectedName, runtime.name); + assertEquals(expectedVendor, runtime.vendor); + } +} diff --git a/components/environment/src/test/java/datadog/environment/JvmOptionsTest.java b/components/environment/src/test/java/datadog/environment/JvmOptionsTest.java new file mode 100644 index 00000000000..52ff039bc9a --- /dev/null +++ b/components/environment/src/test/java/datadog/environment/JvmOptionsTest.java @@ -0,0 +1,38 @@ +package datadog.environment; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class JvmOptionsTest { + static Stream data() { + return Stream.of( + arguments("", emptyList()), + arguments("-Xmx512m", singletonList("-Xmx512m")), + arguments("-Xms256m -Xmx512m", asList("-Xms256m", "-Xmx512m")), + arguments(" -Xms256m -Xmx512m ", asList("-Xms256m", "-Xmx512m")), + arguments("-Xms256m\t-Xmx512m", asList("-Xms256m", "-Xmx512m")), + arguments("\t -Xms256m \t -Xmx512m \t", asList("-Xms256m", "-Xmx512m")), + arguments( + "-Xmx512m -Dprop=\"value with space\"", asList("-Xmx512m", "-Dprop=value with space")), + arguments( + "-Xmx512m -Dprop='value with space'", asList("-Xmx512m", "-Dprop=value with space")), + arguments("-Xmx512m -Dprop='mixing\"quotes'", asList("-Xmx512m", "-Dprop=mixing\"quotes")), + arguments("-Xmx512m -Dprop=\"mixing'quotes\"", asList("-Xmx512m", "-Dprop=mixing'quotes"))); + } + + @ParameterizedTest + @MethodSource("data") + void testParseToolOptions(String javaToolOptions, List expectedVmOptions) { + List vmOptions = JvmOptions.parseToolOptions(javaToolOptions); + assertEquals(expectedVmOptions, vmOptions); + } +} diff --git a/components/environment/src/test/java/datadog/environment/OperatingSystemTest.java b/components/environment/src/test/java/datadog/environment/OperatingSystemTest.java new file mode 100644 index 00000000000..5dc4a355546 --- /dev/null +++ b/components/environment/src/test/java/datadog/environment/OperatingSystemTest.java @@ -0,0 +1,36 @@ +package datadog.environment; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.condition.OS.LINUX; +import static org.junit.jupiter.api.condition.OS.MAC; +import static org.junit.jupiter.api.condition.OS.WINDOWS; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; + +class OperatingSystemTest { + @Test + @EnabledOnOs(LINUX) + void onLinuxOnly() { + assertTrue(OperatingSystem.isLinux()); + assertFalse(OperatingSystem.isMacOs()); + assertFalse(OperatingSystem.isWindows()); + } + + @Test + @EnabledOnOs(MAC) + void onMacOsOnly() { + assertFalse(OperatingSystem.isLinux()); + assertTrue(OperatingSystem.isMacOs()); + assertFalse(OperatingSystem.isWindows()); + } + + @Test + @EnabledOnOs(WINDOWS) + void onWindowsOnly() { + assertFalse(OperatingSystem.isLinux()); + assertFalse(OperatingSystem.isMacOs()); + assertTrue(OperatingSystem.isWindows()); + } +} diff --git a/components/environment/src/test/java/datadog/environment/SystemPropertiesTest.java b/components/environment/src/test/java/datadog/environment/SystemPropertiesTest.java new file mode 100644 index 00000000000..a145071c442 --- /dev/null +++ b/components/environment/src/test/java/datadog/environment/SystemPropertiesTest.java @@ -0,0 +1,38 @@ +package datadog.environment; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.junit.jupiter.api.Test; + +class SystemPropertiesTest { + private static final String EXISTING_SYSTEM_PROPERTY = "java.home"; + private static final String MISSING_SYSTEM_PROPERTY = "undefined.system.property"; + + @Test + void testGet() { + assertNotNull(SystemProperties.get(EXISTING_SYSTEM_PROPERTY)); + assertNull(SystemProperties.get(MISSING_SYSTEM_PROPERTY)); + assertThrows(NullPointerException.class, () -> SystemProperties.get(null)); + } + + @Test + void testGetOrDefault() { + assertNotNull(SystemProperties.getOrDefault(EXISTING_SYSTEM_PROPERTY, null)); + + assertEquals("", SystemProperties.getOrDefault(MISSING_SYSTEM_PROPERTY, "")); + assertNull(SystemProperties.getOrDefault(MISSING_SYSTEM_PROPERTY, null)); + + assertThrows(NullPointerException.class, () -> SystemProperties.getOrDefault(null, "")); + } + + @Test + void testSet() { + String testProperty = "test.property"; + String testValue = "test-value"; + assumeTrue(SystemProperties.get(testProperty) == null); + + assertTrue(SystemProperties.set(testProperty, testValue)); + assertEquals(testValue, SystemProperties.get(testProperty)); + } +} diff --git a/components/environment/src/test/resources/argfiles/space-separated-expected.txt b/components/environment/src/test/resources/argfiles/space-separated-expected.txt new file mode 100644 index 00000000000..9f6e3601e79 --- /dev/null +++ b/components/environment/src/test/resources/argfiles/space-separated-expected.txt @@ -0,0 +1,3 @@ +-Xms256m +-Xmx512m +-Dargfile.prop=test diff --git a/components/environment/src/test/resources/argfiles/space-separated.txt b/components/environment/src/test/resources/argfiles/space-separated.txt new file mode 100644 index 00000000000..40b1252d911 --- /dev/null +++ b/components/environment/src/test/resources/argfiles/space-separated.txt @@ -0,0 +1 @@ +-Xms256m -Xmx512m -Dargfile.prop=test diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Constants.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Constants.java index d6ddee8410d..bec6aa6f148 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Constants.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Constants.java @@ -15,10 +15,11 @@ public final class Constants { */ public static final String[] BOOTSTRAP_PACKAGE_PREFIXES = { "datadog.slf4j", + "datadog.cli", + "datadog.context", + "datadog.environment", "datadog.json", "datadog.yaml", - "datadog.context", - "datadog.cli", "datadog.appsec.api", "datadog.trace.api", "datadog.trace.bootstrap", diff --git a/dd-java-agent/build.gradle b/dd-java-agent/build.gradle index 8592f244c5f..cf35af97164 100644 --- a/dd-java-agent/build.gradle +++ b/dd-java-agent/build.gradle @@ -241,6 +241,7 @@ tasks.withType(GenerateMavenPom).configureEach { task -> dependencies { implementation project(path: ':components:json') implementation project(path: ':components:cli') + implementation project(path: ':components:environment') modules { module("com.squareup.okio:okio") { replacedBy("com.datadoghq.okio:okio") // embed our patched fork diff --git a/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java b/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java index ea784033ebb..fe64b90360d 100644 --- a/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java +++ b/dd-java-agent/src/main/java/datadog/trace/bootstrap/AgentBootstrap.java @@ -4,6 +4,8 @@ import static java.nio.charset.StandardCharsets.UTF_8; import datadog.cli.CLIHelper; +import datadog.environment.EnvironmentVariables; +import datadog.environment.SystemProperties; import de.thetaphi.forbiddenapis.SuppressForbidden; import java.io.BufferedReader; import java.io.File; @@ -63,6 +65,10 @@ public static void premain(final String agentArgs, final Instrumentation inst) { public static void agentmain(final String agentArgs, final Instrumentation inst) { BootstrapInitializationTelemetry initTelemetry; + // TODO Example calls that break Gradle smoke tests + String javaVersion = SystemProperties.get("java.version"); +// String forwarderPath = EnvironmentVariables.get("DD_TELEMETRY_FORWARDER_PATH"); + try { initTelemetry = createInitializationTelemetry(); } catch (Throwable t) { diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/SpockRunner.java b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/SpockRunner.java index 83bf7732644..988b710616e 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/SpockRunner.java +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/SpockRunner.java @@ -38,10 +38,11 @@ public class SpockRunner extends JUnitPlatform { */ public static final String[] BOOTSTRAP_PACKAGE_PREFIXES_COPY = { "datadog.slf4j", + "datadog.cli", + "datadog.context", + "datadog.environment", "datadog.json", "datadog.yaml", - "datadog.context", - "datadog.cli", "datadog.appsec.api", "datadog.trace.api", "datadog.trace.bootstrap", diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index cb46e079d9a..9398720f21f 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -17,6 +17,7 @@ final class CachedData { exclude(project(':internal-api:internal-api-9')) exclude(project(':communication')) exclude(project(':components:context')) + exclude(project(':components:environment')) exclude(project(':components:json')) exclude(project(':components:yaml')) exclude(project(':components:cli')) diff --git a/internal-api/build.gradle b/internal-api/build.gradle index fbf1916a4af..e9ba384aea2 100644 --- a/internal-api/build.gradle +++ b/internal-api/build.gradle @@ -243,9 +243,10 @@ dependencies { // references TraceScope and Continuation from public api api project(':dd-trace-api') api libs.slf4j + api project(':components:cli') api project(':components:context') + api project(':components:environment') api project(':components:yaml') - api project(':components:cli') api project(":utils:time-utils") // has to be loaded by system classloader: diff --git a/settings.gradle b/settings.gradle index 1c20bcacfa1..517619b434e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -88,6 +88,7 @@ include ':dd-java-agent:agent-otel:otel-tooling' include ':communication' include ':components:cli' include ':components:context' +include ':components:environment' include ':components:json' include ':components:yaml' include ':telemetry'