com.oracle.svm.agent.NativeImageAgent Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of svm-agent Show documentation
Show all versions of svm-agent Show documentation
SubstrateVM native-image-agent library
The newest version!
/*
* Copyright (c) 2019, 2021, 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
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.oracle.svm.agent;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import com.oracle.svm.agent.ignoredconfig.AgentMetaInfProcessor;
import com.oracle.svm.agent.stackaccess.InterceptedState;
import com.oracle.svm.agent.stackaccess.EagerlyLoadedJavaStackAccess;
import com.oracle.svm.agent.stackaccess.OnDemandJavaStackAccess;
import com.oracle.svm.agent.tracing.TraceFileWriter;
import com.oracle.svm.agent.tracing.ConfigurationResultWriter;
import com.oracle.svm.agent.tracing.core.Tracer;
import com.oracle.svm.agent.tracing.core.TracingResultWriter;
import com.oracle.svm.core.configure.ConfigurationFile;
import com.oracle.svm.driver.metainf.NativeImageMetaInfWalker;
import org.graalvm.nativeimage.Platform;
import org.graalvm.nativeimage.ProcessProperties;
import org.graalvm.nativeimage.hosted.Feature;
import com.oracle.svm.agent.predicatedconfig.MethodInfoRecordKeeper;
import com.oracle.svm.agent.predicatedconfig.ConfigurationWithOriginsResultWriter;
import com.oracle.svm.configure.config.ConfigurationSet;
import com.oracle.svm.configure.filters.FilterConfigurationParser;
import com.oracle.svm.configure.filters.RuleNode;
import com.oracle.svm.configure.trace.AccessAdvisor;
import com.oracle.svm.configure.trace.TraceProcessor;
import com.oracle.svm.core.SubstrateUtil;
import com.oracle.svm.driver.NativeImage;
import com.oracle.svm.jni.nativeapi.JNIEnvironment;
import com.oracle.svm.jni.nativeapi.JNIJavaVM;
import com.oracle.svm.jni.nativeapi.JNIObjectHandle;
import com.oracle.svm.jvmtiagentbase.JNIHandleSet;
import com.oracle.svm.jvmtiagentbase.JvmtiAgentBase;
import com.oracle.svm.jvmtiagentbase.Support;
import com.oracle.svm.jvmtiagentbase.jvmti.JvmtiEnv;
import com.oracle.svm.jvmtiagentbase.jvmti.JvmtiEventCallbacks;
import com.oracle.svm.jvmtiagentbase.jvmti.JvmtiInterface;
public final class NativeImageAgent extends JvmtiAgentBase {
private static final String AGENT_NAME = "native-image-agent";
private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
private ScheduledThreadPoolExecutor periodicConfigWriterExecutor = null;
private Tracer tracer;
private TracingResultWriter tracingResultWriter;
private Path configOutputDirPath;
private static String getTokenValue(String token) {
return token.substring(token.indexOf('=') + 1);
}
@Override
protected int getRequiredJvmtiVersion() {
return JvmtiInterface.JVMTI_VERSION_1_2;
}
@Override
protected JNIHandleSet constructJavaHandles(JNIEnvironment env) {
return new NativeImageAgentJNIHandleSet(env);
}
@Override
protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks callbacks, String options) {
String traceOutputFile = null;
String configOutputDir = null;
ConfigurationSet mergeConfigs = new ConfigurationSet();
ConfigurationSet omittedConfigs = new ConfigurationSet();
boolean builtinCallerFilter = true;
boolean builtinHeuristicFilter = true;
List callerFilterFiles = new ArrayList<>();
List accessFilterFiles = new ArrayList<>();
boolean experimentalClassLoaderSupport = true;
boolean experimentalClassDefineSupport = false;
boolean experimentalOmitClasspathConfig = false;
boolean build = false;
boolean configurationWithOrigins = false;
int configWritePeriod = -1; // in seconds
int configWritePeriodInitialDelay = 1; // in seconds
boolean trackReflectionMetadata = true;
String[] tokens = !options.isEmpty() ? options.split(",") : new String[0];
for (String token : tokens) {
if (token.startsWith("trace-output=")) {
if (traceOutputFile != null) {
return usage(1, "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=.");
}
configOutputDir = transformPath(getTokenValue(token));
if (token.startsWith("config-merge-dir=")) {
mergeConfigs.addDirectory(Paths.get(configOutputDir));
}
} else if (token.startsWith("config-to-omit=")) {
String omittedConfigDir = getTokenValue(token);
omittedConfigDir = transformPath(omittedConfigDir);
omittedConfigs.addDirectory(Paths.get(omittedConfigDir));
} else if (token.equals("experimental-omit-config-from-classpath")) {
experimentalOmitClasspathConfig = true;
} else if (token.startsWith("experimental-omit-config-from-classpath=")) {
experimentalOmitClasspathConfig = Boolean.parseBoolean(getTokenValue(token));
} else if (token.startsWith("restrict-all-dir") || token.equals("restrict") || token.startsWith("restrict=")) {
warn("restrict mode is no longer supported, ignoring option: " + token);
} else if (token.equals("no-builtin-caller-filter")) {
builtinCallerFilter = false;
} else if (token.startsWith("builtin-caller-filter=")) {
builtinCallerFilter = Boolean.parseBoolean(getTokenValue(token));
} else if (token.equals("no-builtin-heuristic-filter")) {
builtinHeuristicFilter = false;
} else if (token.startsWith("builtin-heuristic-filter=")) {
builtinHeuristicFilter = Boolean.parseBoolean(getTokenValue(token));
} else if (token.equals("no-filter")) { // legacy
builtinCallerFilter = false;
builtinHeuristicFilter = false;
} else if (token.startsWith("no-filter=")) { // legacy
builtinCallerFilter = !Boolean.parseBoolean(getTokenValue(token));
builtinHeuristicFilter = builtinCallerFilter;
} else if (token.startsWith("caller-filter-file=")) {
callerFilterFiles.add(getTokenValue(token));
} else if (token.startsWith("access-filter-file=")) {
accessFilterFiles.add(getTokenValue(token));
} else if (token.equals("experimental-class-loader-support")) {
experimentalClassLoaderSupport = true;
} else if (token.startsWith("experimental-class-loader-support=")) {
experimentalClassLoaderSupport = Boolean.parseBoolean(getTokenValue(token));
} else if (token.equals("experimental-class-define-support")) {
experimentalClassDefineSupport = true;
} else if (token.startsWith("experimental-class-define-support=")) {
experimentalClassDefineSupport = Boolean.parseBoolean(getTokenValue(token));
} 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");
}
} 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");
}
} else if (token.equals("build")) {
build = true;
} else if (token.startsWith("build=")) {
build = Boolean.parseBoolean(getTokenValue(token));
} else if (token.equals("experimental-configuration-with-origins")) {
configurationWithOrigins = true;
} else if (token.equals("track-reflection-metadata")) {
trackReflectionMetadata = true;
} else if (token.startsWith("track-reflection-metadata=")) {
trackReflectionMetadata = Boolean.parseBoolean(getTokenValue(token));
} else {
return usage(1, "unknown option: '" + token + "'.");
}
}
if (traceOutputFile == null && configOutputDir == null && !build) {
configOutputDir = transformPath(AGENT_NAME + "_config-pid{pid}-{datetime}/");
inform("no output/build options provided, tracking dynamic accesses and writing configuration to directory: " + configOutputDir);
}
if (configurationWithOrigins && !mergeConfigs.isEmpty()) {
configurationWithOrigins = false;
inform("using configuration with origins with configuration merging is currently unsupported. Disabling configuration with origins mode.");
}
if (configurationWithOrigins) {
warn("using experimental configuration with origins mode. Note that native-image cannot process these files, and this flag may change or be removed without a warning!");
}
RuleNode callerFilter = null;
if (!builtinCallerFilter) {
callerFilter = RuleNode.createRoot();
callerFilter.addOrGetChildren("**", RuleNode.Inclusion.Include);
}
if (!callerFilterFiles.isEmpty()) {
if (callerFilter == null) {
callerFilter = AccessAdvisor.copyBuiltinCallerFilterTree();
}
if (!parseFilterFiles(callerFilter, callerFilterFiles)) {
return 1;
}
}
RuleNode accessFilter = null;
if (!accessFilterFiles.isEmpty()) {
accessFilter = AccessAdvisor.copyBuiltinAccessFilterTree();
if (!parseFilterFiles(accessFilter, accessFilterFiles)) {
return 1;
}
}
final MethodInfoRecordKeeper recordKeeper = new MethodInfoRecordKeeper(configurationWithOrigins);
final Supplier interceptedStateSupplier = configurationWithOrigins ? EagerlyLoadedJavaStackAccess.stackAccessSupplier()
: OnDemandJavaStackAccess.stackAccessSupplier();
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=.");
}
try {
configOutputDirPath = Paths.get(configOutputDir);
if (!Files.exists(configOutputDirPath)) {
Files.createDirectories(configOutputDirPath);
}
if (experimentalOmitClasspathConfig) {
ignoreConfigFromClasspath(jvmti, omittedConfigs);
}
AccessAdvisor advisor = createAccessAdvisor(builtinHeuristicFilter, callerFilter, accessFilter);
TraceProcessor omittedConfigProcessor = null;
Predicate shouldExcludeClassesWithHash = null;
if (!omittedConfigs.isEmpty()) {
Function ignore = e -> {
warn("Failed to load omitted config: " + e);
return null;
};
omittedConfigProcessor = new TraceProcessor(advisor, omittedConfigs.loadJniConfig(ignore), omittedConfigs.loadReflectConfig(ignore),
omittedConfigs.loadProxyConfig(ignore), omittedConfigs.loadResourceConfig(ignore), omittedConfigs.loadSerializationConfig(ignore),
omittedConfigs.loadPredefinedClassesConfig(null, null, ignore), null);
shouldExcludeClassesWithHash = omittedConfigProcessor.getPredefinedClassesConfiguration()::containsClassWithHash;
}
Path[] predefinedClassDestinationDirs = {configOutputDirPath.resolve(ConfigurationFile.PREDEFINED_CLASSES_AGENT_EXTRACTED_SUBDIR)};
if (configurationWithOrigins) {
ConfigurationWithOriginsResultWriter writer = new ConfigurationWithOriginsResultWriter(advisor, recordKeeper);
tracer = writer;
tracingResultWriter = writer;
} else {
Function handler = e -> {
if (e instanceof NoSuchFileException) {
warn("file " + ((NoSuchFileException) e).getFile() + " for merging could not be found, skipping");
return null;
} else if (e instanceof FileNotFoundException) {
warn("could not open configuration file: " + e);
return null;
}
return e; // rethrow
};
TraceProcessor processor = new TraceProcessor(advisor, mergeConfigs.loadJniConfig(handler), mergeConfigs.loadReflectConfig(handler),
mergeConfigs.loadProxyConfig(handler), mergeConfigs.loadResourceConfig(handler), mergeConfigs.loadSerializationConfig(handler),
mergeConfigs.loadPredefinedClassesConfig(predefinedClassDestinationDirs, shouldExcludeClassesWithHash, handler), omittedConfigProcessor);
ConfigurationResultWriter writer = new ConfigurationResultWriter(processor);
tracer = writer;
tracingResultWriter = writer;
}
} catch (Throwable t) {
return error(2, t.toString());
}
} else if (traceOutputFile != null) {
try {
Path path = Paths.get(transformPath(traceOutputFile));
TraceFileWriter writer = new TraceFileWriter(path);
tracer = writer;
tracingResultWriter = writer;
} catch (Throwable t) {
return error(2, t.toString());
}
}
if (build) {
int status = buildImage(jvmti);
if (status == 0) {
System.exit(status);
}
return status;
}
try {
BreakpointInterceptor.onLoad(jvmti, callbacks, tracer, this, interceptedStateSupplier,
experimentalClassLoaderSupport, experimentalClassDefineSupport, trackReflectionMetadata);
} catch (Throwable t) {
return error(3, t.toString());
}
try {
JniCallInterceptor.onLoad(tracer, this, interceptedStateSupplier);
} catch (Throwable t) {
return error(4, t.toString());
}
setupExecutorServiceForPeriodicConfigurationCapture(configWritePeriod, configWritePeriodInitialDelay);
return 0;
}
private static void inform(String message) {
System.err.println(AGENT_NAME + ": " + message);
}
private static void warn(String message) {
inform("Warning: " + message);
}
private static T error(T result, String message) {
inform("Error: " + message);
return result;
}
private static T usage(T result, String message) {
inform(message);
inform("Example usage: -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/");
inform("For details, please read BuildConfiguration.md or https://www.graalvm.org/reference-manual/native-image/BuildConfiguration/");
return result;
}
private static AccessAdvisor createAccessAdvisor(boolean builtinHeuristicFilter, RuleNode callerFilter, RuleNode accessFilter) {
AccessAdvisor advisor = new AccessAdvisor();
advisor.setHeuristicsEnabled(builtinHeuristicFilter);
if (callerFilter != null) {
advisor.setCallerFilterTree(callerFilter);
}
if (accessFilter != null) {
advisor.setAccessFilterTree(accessFilter);
}
return advisor;
}
private static int parseIntegerOrNegative(String number) {
try {
return Integer.parseInt(number);
} catch (NumberFormatException ex) {
return -1;
}
}
private static boolean parseFilterFiles(RuleNode filter, List filterFiles) {
for (String path : filterFiles) {
try {
new FilterConfigurationParser(filter).parseAndRegister(Paths.get(path));
} catch (Exception e) {
return error(false, "cannot parse filter file " + path + ": " + e);
}
}
filter.removeRedundantNodes();
return true;
}
private void setupExecutorServiceForPeriodicConfigurationCapture(int writePeriod, int initialDelay) {
if (tracingResultWriter == null || configOutputDirPath == null || !tracingResultWriter.supportsPeriodicTraceWriting()) {
return;
}
// No periodic writing of files by default
if (writePeriod == -1) {
return;
}
periodicConfigWriterExecutor = new ScheduledThreadPoolExecutor(1, r -> {
Thread workerThread = new Thread(r);
workerThread.setDaemon(true);
workerThread.setName("AgentConfigurationsPeriodicWriter");
return workerThread;
});
periodicConfigWriterExecutor.setRemoveOnCancelPolicy(true);
periodicConfigWriterExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
periodicConfigWriterExecutor.scheduleAtFixedRate(this::writeConfigurationFiles,
initialDelay, writePeriod, TimeUnit.SECONDS);
}
private static void ignoreConfigFromClasspath(JvmtiEnv jvmti, ConfigurationSet ignoredConfigSet) {
String classpath = Support.getSystemProperty(jvmti, "java.class.path");
String sep = Support.getSystemProperty(jvmti, "path.separator");
if (sep == null) {
if (Platform.includedIn(Platform.LINUX.class) || Platform.includedIn(Platform.DARWIN.class)) {
sep = ":";
} else if (Platform.includedIn(Platform.WINDOWS.class)) {
sep = "[:;]";
} else {
warn("Running on unknown platform. Not omitting existing config from classpath.");
return;
}
}
AgentMetaInfProcessor processor = new AgentMetaInfProcessor(ignoredConfigSet);
for (String cpEntry : classpath.split(sep)) {
try {
NativeImageMetaInfWalker.walkMetaInfForCPEntry(Paths.get(cpEntry), processor);
} catch (NativeImageMetaInfWalker.MetaInfWalkException e) {
warn("Failed to walk the classpath entry: " + cpEntry + " Reason: " + e);
}
}
}
private static final Pattern propertyBlacklist = Pattern.compile("(java\\..*)|(sun\\..*)|(jvmci\\..*)");
private static final Pattern propertyWhitelist = Pattern.compile("(java\\.library\\.path)|(java\\.io\\.tmpdir)");
private static int buildImage(JvmtiEnv jvmti) {
System.out.println("Building native image ...");
String classpath = Support.getSystemProperty(jvmti, "java.class.path");
if (classpath == null) {
return usage(1, "Build mode could not determine classpath.");
}
String javaCommand = Support.getSystemProperty(jvmti, "sun.java.command");
String mainClassMissing = "Build mode could not determine main class.";
if (javaCommand == null) {
return usage(1, mainClassMissing);
}
String mainClass = SubstrateUtil.split(javaCommand, " ")[0];
if (mainClass.isEmpty()) {
return usage(1, mainClassMissing);
}
List buildArgs = new ArrayList<>();
// buildArgs.add("--verbose");
String[] keys = Support.getSystemProperties(jvmti);
for (String key : keys) {
boolean whitelisted = propertyWhitelist.matcher(key).matches();
boolean blacklisted = !whitelisted && propertyBlacklist.matcher(key).matches();
if (blacklisted) {
continue;
}
buildArgs.add("-D" + key + "=" + Support.getSystemProperty(jvmti, key));
}
if (mainClass.toLowerCase().endsWith(".jar")) {
buildArgs.add("-jar");
} else {
buildArgs.addAll(Arrays.asList("-cp", classpath));
}
buildArgs.add(mainClass);
buildArgs.add(AGENT_NAME + ".build");
// System.out.println(String.join("\n", buildArgs));
Path javaHome = Paths.get(Support.getSystemProperty(jvmti, "java.home"));
String userDirStr = Support.getSystemProperty(jvmti, "user.dir");
NativeImage.agentBuild(javaHome, userDirStr == null ? null : Paths.get(userDirStr), buildArgs);
return 0;
}
private static String transformPath(String path) {
String result = path;
if (result.contains("{pid}")) {
result = result.replace("{pid}", Long.toString(ProcessProperties.getProcessID()));
}
if (result.contains("{datetime}")) {
DateFormat fmt = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
fmt.setTimeZone(UTC_TIMEZONE);
result = result.replace("{datetime}", fmt.format(new Date()));
}
return result;
}
@Override
protected void onVMInitCallback(JvmtiEnv jvmti, JNIEnvironment jni, JNIObjectHandle thread) {
BreakpointInterceptor.onVMInit(jvmti, jni);
if (tracer != null) {
tracer.tracePhaseChange("live");
}
}
@Override
protected void onVMStartCallback(JvmtiEnv jvmti, JNIEnvironment jni) {
JniCallInterceptor.onVMStart(jvmti);
if (tracer != null) {
tracer.tracePhaseChange("start");
}
}
@Override
protected void onVMDeathCallback(JvmtiEnv jvmti, JNIEnvironment jni) {
if (tracer != null) {
tracer.tracePhaseChange("dead");
}
}
private static final int MAX_WARNINGS_FOR_WRITING_CONFIGS_FAILURES = 5;
private static int currentFailuresWritingConfigs = 0;
private void writeConfigurationFiles() {
try {
final Path tempDirectory = configOutputDirPath.toFile().exists()
? Files.createTempDirectory(configOutputDirPath, "tempConfig-")
: Files.createTempDirectory("tempConfig-");
List writtenFilePaths = tracingResultWriter.writeToDirectory(tempDirectory);
for (Path writtenFilePath : writtenFilePaths) {
Path fileName = tempDirectory.relativize(writtenFilePath);
Path target = configOutputDirPath.resolve(fileName);
tryAtomicMove(writtenFilePath, target);
}
/*
* Note that sidecar files may be written directly to the final output directory, such
* as the class files from predefined class tracking. However, such files generally
* don't change once they have been written.
*/
compulsoryDelete(tempDirectory);
} catch (IOException e) {
warnUpToLimit(currentFailuresWritingConfigs++, MAX_WARNINGS_FOR_WRITING_CONFIGS_FAILURES, "Error when writing configuration files: " + e.toString());
}
}
private static void compulsoryDelete(Path pathToDelete) {
final int maxRetries = 3;
int retries = 0;
while (pathToDelete.toFile().exists() && !pathToDelete.toFile().delete() && retries < maxRetries) {
retries++;
}
}
private static void warnUpToLimit(int currentCount, int limit, String message) {
if (currentCount < limit) {
warn(message);
return;
}
if (currentCount == limit) {
warn(message);
warn("The above warning will no longer be reported.");
}
}
private static final int MAX_FAILURES_ATOMIC_MOVE = 20;
private static int currentFailuresAtomicMove = 0;
private static void tryAtomicMove(final Path source, final Path target) throws IOException {
try {
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
warnUpToLimit(currentFailuresAtomicMove++, MAX_FAILURES_ATOMIC_MOVE,
String.format("Could not move temporary configuration profile from (%s) to (%s) atomically. " +
"This might result in inconsistencies.", source.toAbsolutePath(), target.toAbsolutePath()));
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
}
}
@Override
protected int onUnloadCallback(JNIJavaVM vm) {
if (periodicConfigWriterExecutor != null) {
periodicConfigWriterExecutor.shutdown();
try {
periodicConfigWriterExecutor.awaitTermination(300, TimeUnit.MILLISECONDS);
} catch (InterruptedException ex) {
periodicConfigWriterExecutor.shutdownNow();
}
}
if (tracer != null) {
tracer.tracePhaseChange("unload");
}
if (tracingResultWriter != null) {
tracingResultWriter.close();
if (tracingResultWriter.supportsOnUnloadTraceWriting()) {
if (configOutputDirPath != null) {
writeConfigurationFiles();
configOutputDirPath = null;
}
}
}
/*
* Agent shutdown is tricky: apparently we can still have events at the same time as this
* function executes, so we would need to synchronize. We could do this with a combined
* shared+exclusive lock, but that adds some cost to all events. We choose to leak a few
* handles and some memory for now -- this agent isn't supposed to be attached only
* temporarily anyway, and the impending process exit should free any resources we take
* (unless another JVM is launched in this process).
*/
// cleanupOnUnload(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;
}
@SuppressWarnings("unused")
private static void cleanupOnUnload(JNIJavaVM vm) {
JniCallInterceptor.onUnload();
BreakpointInterceptor.onUnload();
}
@SuppressWarnings("unused")
public static class RegistrationFeature implements Feature {
@Override
public void afterRegistration(AfterRegistrationAccess access) {
JvmtiAgentBase.registerAgent(new NativeImageAgent());
}
}
}