Skip to content

Commit

Permalink
[GR-59864] Improve handling of agent and host VM version mismatches
Browse files Browse the repository at this point in the history
PullRequest: graal/19466
  • Loading branch information
DSouzaM committed Jan 16, 2025
2 parents a44c46a + 00f1d4f commit d9266a9
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 23 deletions.
1 change: 1 addition & 0 deletions substratevm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This changelog summarizes major changes to GraalVM Native Image.
* (GR-58668) Enabled [Whole-Program Sparse Conditional Constant Propagation (WP-SCCP)](https://github.com/oracle/graal/pull/9821) by default, improving the precision of points-to analysis in Native Image. This optimization enhances static analysis accuracy and scalability, potentially reducing the size of the final native binary.
* (GR-59313) Deprecated class-level metadata extraction using `native-image-inspect` and removed option `DumpMethodsData`. Use class-level SBOMs instead by passing `--enable-sbom=class-level,export` to the `native-image` builder. The default value of option `IncludeMethodData` was changed to `false`.
* (GR-52400) The build process now uses 85% of system memory in containers and CI environments. Otherwise, it tries to only use available memory. If less than 8GB of memory are available, it falls back to 85% of system memory. The reason for the selected memory limit is now also shown in the build resources section of the build output.
* (GR-59864) Added JVM version check to the Native Image agent. The agent will abort execution if the JVM major version does not match the version it was built with, and warn if the full JVM version is different.

## GraalVM for JDK 24 (Internal Version 24.2.0)
* (GR-59717) Added `DuringSetupAccess.registerObjectReachabilityHandler` to allow registering a callback that is executed when an object of a specified type is marked as reachable during heap scanning.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand All @@ -24,6 +24,11 @@
*/
package com.oracle.svm.agent;

import static com.oracle.svm.agent.NativeImageAgent.ExitCodes.AGENT_ERROR;
import static com.oracle.svm.agent.NativeImageAgent.ExitCodes.PARSE_ERROR;
import static com.oracle.svm.agent.NativeImageAgent.ExitCodes.SUCCESS;
import static com.oracle.svm.agent.NativeImageAgent.ExitCodes.USAGE_ERROR;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
Expand Down Expand Up @@ -154,12 +159,12 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
for (String token : tokens) {
if (token.startsWith("trace-output=")) {
if (traceOutputFile != null) {
return usage(1, "cannot specify trace-output= more than once.");
return usage("cannot specify trace-output= more than once.");
}
traceOutputFile = getTokenValue(token);
} else if (token.startsWith("config-output-dir=") || token.startsWith("config-merge-dir=")) {
if (configOutputDir != null) {
return usage(1, "cannot specify more than one of config-output-dir= or config-merge-dir=.");
return usage("cannot specify more than one of config-output-dir= or config-merge-dir=.");
}
configOutputDir = transformPath(getTokenValue(token));
if (token.startsWith("config-merge-dir=")) {
Expand Down Expand Up @@ -197,12 +202,12 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
} else if (token.startsWith("config-write-period-secs=")) {
configWritePeriod = parseIntegerOrNegative(getTokenValue(token));
if (configWritePeriod <= 0) {
return usage(1, "config-write-period-secs must be an integer greater than 0");
return usage("config-write-period-secs must be an integer greater than 0");
}
} else if (token.startsWith("config-write-initial-delay-secs=")) {
configWritePeriodInitialDelay = parseIntegerOrNegative(getTokenValue(token));
if (configWritePeriodInitialDelay < 0) {
return usage(1, "config-write-initial-delay-secs must be an integer greater or equal to 0");
return usage("config-write-initial-delay-secs must be an integer greater or equal to 0");
}
} else if (isBooleanOption(token, "experimental-configuration-with-origins")) {
configurationWithOrigins = getBooleanTokenValue(token);
Expand All @@ -215,17 +220,21 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
} else if (isBooleanOption(token, "track-reflection-metadata")) {
trackReflectionMetadata = getBooleanTokenValue(token);
} else {
return usage(1, "unknown option: '" + token + "'.");
return usage("unknown option: '" + token + "'.");
}
}

if (!checkJVMVersion(jvmti)) {
return USAGE_ERROR;
}

if (traceOutputFile == null && configOutputDir == null) {
configOutputDir = transformPath(AGENT_NAME + "_config-pid{pid}-{datetime}/");
inform("no output options provided, tracking dynamic accesses and writing configuration to directory: " + configOutputDir);
}

if (configurationWithOrigins && !conditionalConfigUserPackageFilterFiles.isEmpty()) {
return error(5, "The agent can only be used in either the configuration with origins mode or the predefined classes mode.");
return error(USAGE_ERROR, "The agent can only be used in either the configuration with origins mode or the predefined classes mode.");
}

if (configurationWithOrigins && !mergeConfigs.isEmpty()) {
Expand All @@ -250,20 +259,20 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
callerFilter = new ComplexFilter(callerFilterHierarchyFilterNode);
}
if (!parseFilterFiles(callerFilter, callerFilterFiles)) {
return 1;
return PARSE_ERROR;
}
}

ComplexFilter accessFilter = null;
if (!accessFilterFiles.isEmpty()) {
accessFilter = new ComplexFilter(AccessAdvisor.copyBuiltinAccessFilterTree());
if (!parseFilterFiles(accessFilter, accessFilterFiles)) {
return 1;
return PARSE_ERROR;
}
}

if (!conditionalConfigUserPackageFilterFiles.isEmpty() && conditionalConfigPartialRun) {
return error(6, "The agent can generate conditional configuration either for the current run or in the partial mode but not both at the same time.");
return error(USAGE_ERROR, "The agent can generate conditional configuration either for the current run or in the partial mode but not both at the same time.");
}

boolean isConditionalConfigurationRun = !conditionalConfigUserPackageFilterFiles.isEmpty() || conditionalConfigPartialRun;
Expand All @@ -274,7 +283,7 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c

if (configOutputDir != null) {
if (traceOutputFile != null) {
return usage(1, "can only once specify exactly one of trace-output=, config-output-dir= or config-merge-dir=.");
return usage("can only once specify exactly one of trace-output=, config-output-dir= or config-merge-dir=.");
}
try {
configOutputDirPath = Files.createDirectories(Path.of(configOutputDir));
Expand All @@ -289,7 +298,7 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
} catch (Exception ignored) {
process = "(unknown)";
}
return error(2, "Output directory '" + configOutputDirPath + "' is locked by process " + process + ", " +
return error(AGENT_ERROR, "Output directory '" + configOutputDirPath + "' is locked by process " + process + ", " +
"which means another agent instance is already writing to this directory. " +
"Only one agent instance can safely write to a specific target directory at the same time. " +
"Unless file '" + ConfigurationFile.LOCK_FILE_NAME + "' is a leftover from an earlier process that terminated abruptly, it is unsafe to delete it. " +
Expand Down Expand Up @@ -322,13 +331,13 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
} else {
ComplexFilter userCodeFilter = new ComplexFilter(HierarchyFilterNode.createRoot());
if (!parseFilterFiles(userCodeFilter, conditionalConfigUserPackageFilterFiles)) {
return 2;
return PARSE_ERROR;
}
ComplexFilter classNameFilter;
if (!conditionalConfigClassNameFilterFiles.isEmpty()) {
classNameFilter = new ComplexFilter(HierarchyFilterNode.createRoot());
if (!parseFilterFiles(classNameFilter, conditionalConfigClassNameFilterFiles)) {
return 3;
return PARSE_ERROR;
}
} else {
classNameFilter = new ComplexFilter(HierarchyFilterNode.createInclusiveRoot());
Expand Down Expand Up @@ -360,16 +369,16 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
}
expectedConfigModifiedBefore = getMostRecentlyModified(configOutputDirPath, getMostRecentlyModified(configOutputLockFilePath, null));
} catch (Throwable t) {
return error(2, t.toString());
return error(AGENT_ERROR, t.toString());
}
} else if (traceOutputFile != null) {
} else {
try {
Path path = Paths.get(transformPath(traceOutputFile));
TraceFileWriter writer = new TraceFileWriter(path);
tracer = writer;
tracingResultWriter = writer;
} catch (Throwable t) {
return error(2, t.toString());
return error(AGENT_ERROR, t.toString());
}
}

Expand All @@ -381,16 +390,16 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
BreakpointInterceptor.onLoad(jvmti, callbacks, tracer, this, interceptedStateSupplier,
experimentalClassLoaderSupport, experimentalClassDefineSupport, experimentalUnsafeAllocationSupport, trackReflectionMetadata);
} catch (Throwable t) {
return error(3, t.toString());
return error(AGENT_ERROR, t.toString());
}
try {
JniCallInterceptor.onLoad(tracer, this, interceptedStateSupplier);
} catch (Throwable t) {
return error(4, t.toString());
return error(AGENT_ERROR, t.toString());
}

setupExecutorServiceForPeriodicConfigurationCapture(configWritePeriod, configWritePeriodInitialDelay);
return 0;
return SUCCESS;
}

private static void inform(String message) {
Expand All @@ -408,11 +417,39 @@ private static <T> T error(T result, String message) {
return result;
}

private static <T> T usage(T result, String message) {
private static int usage(String message) {
inform(message);
inform("Example usage: -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/");
inform("For details, please read AutomaticMetadataCollection.md or https://www.graalvm.org/dev/reference-manual/native-image/metadata/AutomaticMetadataCollection/");
return result;
return USAGE_ERROR;
}

private static boolean checkJVMVersion(JvmtiEnv jvmti) {
String agentVersion = System.getProperty("java.vm.version");
int agentMajorVersion = Runtime.version().feature();

String vmVersion = Support.getSystemProperty(jvmti, "java.vm.version");
if (vmVersion == null) {
warn(String.format("Unable to determine the \"java.vm.version\" of the running JVM. Note that the JVM should have major version %d, otherwise metadata may be incorrect.",
agentMajorVersion));
return true;
}

// Fail if the major versions differ.
String[] parts = vmVersion.split("\\D");
if (parts.length == 0 || Integer.parseInt(parts[0]) != agentMajorVersion) {
return error(false, String.format(
"The current VM (%s) is incompatible with the agent, which was built for a JVM with major version %d. To resolve this issue, run the agent using a JVM with major version %d.",
vmVersion, agentMajorVersion, agentMajorVersion));
}

// Warn if the VM is different.
if (!vmVersion.startsWith(agentVersion)) {
warn(String.format(
"The running JVM (%s) is different from the JVM used to build the agent (%s). If the generated metadata is incorrect or incomplete, consider running the agent using the same JVM that built it.",
vmVersion, agentVersion));
}
return true;
}

private static AccessAdvisor createAccessAdvisor(boolean builtinHeuristicFilter, ConfigurationFilter callerFilter, ConfigurationFilter accessFilter) {
Expand Down Expand Up @@ -689,7 +726,14 @@ protected int onUnloadCallback(JNIJavaVM vm) {
* The epilogue of this method does not tear down our VM: we don't seem to observe all
* threads that end and therefore can't detach them, so we would wait forever for them.
*/
return 0;
return SUCCESS;
}

static class ExitCodes {
static final int SUCCESS = 0;
static final int USAGE_ERROR = 1;
static final int PARSE_ERROR = 2;
static final int AGENT_ERROR = 3;
}

@SuppressWarnings("unused")
Expand Down

0 comments on commit d9266a9

Please sign in to comment.