-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Support agent by attaching it at build time #9668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
c2c3e23
7e87af1
592d027
ff19216
c4900b2
3cc16af
9ae8190
260d4f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1820,10 +1820,14 @@ def cinterfacetutorial(args): | |
| @mx.command(suite.name, 'javaagenttest', 'Runs tests for java agent with native image') | ||
| def java_agent_test(args): | ||
| def build_and_run(args, binary_path, native_image, agents, agents_arg): | ||
| test_cp = os.pathsep.join([classpath('com.oracle.svm.test')] + agents) | ||
| mx.log('Run agent with JVM as baseline') | ||
| test_cp = os.pathsep.join([classpath('com.oracle.svm.test')]) | ||
| java_run_cp = os.pathsep.join([test_cp, mx.dependency('org.graalvm.nativeimage').classpath_repr()]) | ||
| mx.run_java( agents_arg + ['-cp', java_run_cp, 'com.oracle.svm.test.javaagent.AgentTest']) | ||
| test_cp = os.pathsep.join([test_cp] + agents) | ||
| native_agent_premain_options = ['-XXpremain:com.oracle.svm.test.javaagent.agent1.TestJavaAgent1:test.agent1=true', '-XXpremain:com.oracle.svm.test.javaagent.agent2.TestJavaAgent2:test.agent2=true'] | ||
| image_args = ['-cp', test_cp, '-J-ea', '-J-esa', '-H:+ReportExceptionStackTraces', '-H:Class=com.oracle.svm.test.javaagent.AgentTest'] | ||
| native_image(image_args + svm_experimental_options(['-H:PremainClasses=' + agents_arg]) + ['-o', binary_path] + args) | ||
| native_image(image_args + svm_experimental_options(agents_arg) + ['-o', binary_path] + args) | ||
| mx.run([binary_path] + native_agent_premain_options) | ||
|
|
||
| def build_and_test_java_agent_image(native_image, args): | ||
|
|
@@ -1840,18 +1844,24 @@ def build_and_test_java_agent_image(native_image, args): | |
| # Note: we are not using MX here to avoid polluting the suite.py and requiring extra build flags | ||
| mx.log("Building agent jars from " + test_classpath) | ||
| agents = [] | ||
| for i in range(1, 2): | ||
| for i in range(1, 3): | ||
| agent = join(tmp_dir, "testagent%d.jar" % (i)) | ||
| agent_test_classpath = join(test_classpath, 'com', 'oracle', 'svm', 'test', 'javaagent', 'agent' + str(i)) | ||
| class_list = [join(test_classpath, 'com', 'oracle', 'svm', 'test', 'javaagent', 'agent' + str(i), f) for f in os.listdir(agent_test_classpath) if os.path.isfile(os.path.join(agent_test_classpath, f)) and f.endswith(".class")] | ||
| mx.run([mx.get_jdk().jar, 'cmf', join(test_classpath, 'resources', 'javaagent' + str(i), 'MANIFEST.MF'), agent] + class_list, cwd = tmp_dir) | ||
| current_dir = os.getcwd() | ||
| # Change to test classpath to create agent jar file | ||
| os.chdir(test_classpath) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to change the dir?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Running |
||
| agent_test_classpath = join('com', 'oracle', 'svm', 'test', 'javaagent', 'agent' + str(i)) | ||
| class_list = [join('com', 'oracle', 'svm', 'test', 'javaagent', 'agent' + str(i), f) for f in os.listdir(agent_test_classpath) if os.path.isfile(os.path.join(agent_test_classpath, f)) and f.endswith(".class")] | ||
| class_list.append(join('com', 'oracle', 'svm', 'test', 'javaagent', 'AgentPremainHelper.class')) | ||
| class_list.append(join('com', 'oracle', 'svm', 'test', 'javaagent', 'AssertInAgent.class')) | ||
| mx.run([mx.get_jdk().jar, 'cmf', join(test_classpath, 'resources', 'javaagent' + str(i), 'MANIFEST.MF'), agent] + class_list, cwd = test_classpath) | ||
| agents.append(agent) | ||
| os.chdir(current_dir) | ||
|
|
||
| mx.log("Building images with different agent orders ") | ||
| build_and_run(args, join(tmp_dir, 'agenttest1'), native_image, agents,'com.oracle.svm.test.javaagent.agent1.TestJavaAgent1,com.oracle.svm.test.javaagent.agent2.TestJavaAgent2') | ||
| build_and_run(args, join(tmp_dir, 'agenttest1'), native_image, agents,[f'-javaagent:{agents[0]}=test.agent1=true', f'-javaagent:{agents[1]}=test.agent2=true']) | ||
|
|
||
| # Switch the premain sequence of agent1 and agent2 | ||
| build_and_run(args, join(tmp_dir, 'agenttest2'), native_image, agents, 'com.oracle.svm.test.javaagent.agent2.TestJavaAgent2,com.oracle.svm.test.javaagent.agent1.TestJavaAgent1') | ||
| build_and_run(args, join(tmp_dir, 'agenttest2'), native_image, agents, [f'-javaagent:{agents[1]}=test.agent2=true', f'-javaagent:{agents[0]}=test.agent1=true']) | ||
|
|
||
| native_image_context_run(build_and_test_java_agent_image, args) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -224,6 +224,13 @@ protected void onValueUpdate(EconomicMap<OptionKey<?>, Object> values, String ol | |
| public static OptionEnabledHandler<Boolean> imageLayerEnabledHandler; | ||
| public static OptionEnabledHandler<Boolean> imageLayerCreateEnabledHandler; | ||
|
|
||
| @APIOption(name = "-javaagent", valueSeparator = ':')// | ||
| @Option(help = "Enable the specified java agent in native image. Usage: -javaagent:<jarpath>[=<options>]. " + | ||
| "The java agent will run at image build time to take its effects in the output native image. " + | ||
| "Be noticed: The java agent's premain method will be re-executed at native image runtime. " + | ||
| "The agent should isolate the executions according to runtime environment.", type = User, stability = OptionStability.EXPERIMENTAL)// | ||
| public static final HostedOptionKey<AccumulatingLocatableMultiOptionValue.Strings> JavaAgent = new HostedOptionKey<>(AccumulatingLocatableMultiOptionValue.Strings.build()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This partially conflicts with the basic JVMTI runtime support that was merged a couple weeks ago (see #9558). I will need to think about that a bit and I will try to come up with an approach to unify the JVMTI build- and runtime support.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where does the conflict come from? Feels to me that with
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there is a conflict, as you said they are working on different phases. Class transformation has nothing to do with JVMTI events.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both JVMTI and Java agents can be used to instrument Java classes. I was worried about those approaches conflicting. However, I think that you are right, this probably shouldn't result in worse situations than on HotSpot. Besides that, the same restrictions apply to both instrumentation approaches, so there is also no difference in terms of behavior there:
|
||
|
|
||
| @Fold | ||
| public static boolean getSourceLevelDebug() { | ||
| return SourceLevelDebug.getValue(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,33 +35,26 @@ | |
| import org.graalvm.nativeimage.hosted.Feature; | ||
|
|
||
| import com.oracle.svm.core.PreMainSupport; | ||
| import com.oracle.svm.core.SubstrateOptions; | ||
| import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; | ||
| import com.oracle.svm.core.feature.InternalFeature; | ||
| import com.oracle.svm.core.option.AccumulatingLocatableMultiOptionValue; | ||
| import com.oracle.svm.core.option.HostedOptionKey; | ||
| import com.oracle.svm.core.option.SubstrateOptionsParser; | ||
| import com.oracle.svm.core.util.BasedOnJDKFile; | ||
| import com.oracle.svm.core.util.UserError; | ||
| import com.oracle.svm.hosted.reflect.ReflectionFeature; | ||
|
|
||
| import jdk.graal.compiler.options.Option; | ||
| import jdk.graal.compiler.options.OptionStability; | ||
| import java.io.IOException; | ||
| import java.util.jar.JarFile; | ||
|
|
||
| /** | ||
| * This feature supports instrumentation in native image. | ||
| */ | ||
| @AutomaticallyRegisteredFeature | ||
| public class InstrumentFeature implements InternalFeature { | ||
| public static class Options { | ||
| @Option(help = "Specify premain-class list. Multiple classes are separated by comma, and order matters. This is an experimental option.", stability = OptionStability.EXPERIMENTAL)// | ||
| public static final HostedOptionKey<AccumulatingLocatableMultiOptionValue.Strings> PremainClasses = new HostedOptionKey<>( | ||
| AccumulatingLocatableMultiOptionValue.Strings.buildWithCommaDelimiter()); | ||
|
|
||
| } | ||
|
|
||
| @Override | ||
| public boolean isInConfiguration(IsInConfigurationAccess access) { | ||
| return !Options.PremainClasses.getValue().values().isEmpty(); | ||
| return !SubstrateOptions.JavaAgent.getValue().values().isEmpty(); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -77,28 +70,50 @@ public void afterRegistration(AfterRegistrationAccess access) { | |
| PreMainSupport support = new PreMainSupport(); | ||
| ImageSingletons.add(PreMainSupport.class, support); | ||
|
|
||
| List<String> premainClasses = Options.PremainClasses.getValue().values(); | ||
| for (String clazz : premainClasses) { | ||
| addPremainClass(support, cl, clazz); | ||
| List<String> agentOptions = SubstrateOptions.JavaAgent.getValue().values(); | ||
| for (String agentOption : agentOptions) { | ||
| addPremainClass(support, cl, agentOption); | ||
| } | ||
| } | ||
|
|
||
| private static void addPremainClass(PreMainSupport support, ClassLoader cl, String premainClass) { | ||
| private static void addPremainClass(PreMainSupport support, ClassLoader cl, String javaagentOption) { | ||
| int separatorIndex = javaagentOption.indexOf("="); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can probably use
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The jar file path which is prior to the first |
||
| String agent; | ||
| String premainClass = null; | ||
| String options = ""; | ||
| // Get the agent file | ||
| if (separatorIndex == -1) { | ||
| agent = javaagentOption; | ||
| } else { | ||
| agent = javaagentOption.substring(0, separatorIndex); | ||
| options = javaagentOption.substring(separatorIndex + 1); | ||
| } | ||
| // Read MANIFEST in agent jar | ||
| try { | ||
| JarFile agentJarFile = new JarFile(agent); | ||
| premainClass = agentJarFile.getManifest().getMainAttributes().getValue("Premain-Class"); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens when there is no Manifest? There should be a special error message for that.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this moment, the java agent has been already loaded and run by JVM.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, HotSpot already executed the method I think that this will only work for a very small number of Java agents (i.e., agents that contain Native Image-specific logic or that hardly execute any code in Ideally, the agent manifest would contained some information to identify if the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From users' aspect, they need to manually isolate premain logic in JVM runtime and native image runtime anyway, a smoother and more convenient approach would be better. It is possible some simple agents don't have complicated framework to init in
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @christianhaeubl the correct solution here would be that the Java agents don't do both transformation and initialization from the We are hoping here that most of the users won't need to re-write their agents as the We are implementing this as an experimental feature to experiment, and if it works great. If most of the agents need to be rewritten, then I would go with a two-method solution that you propose. |
||
| } catch (IOException e) { | ||
| // This should never happen because the image build process (HotSpot) already loaded the | ||
| // agent during startup. | ||
| throw UserError.abort(e, "Can't read the agent jar %s. Please check option %s", agent, | ||
| SubstrateOptionsParser.commandArgument(SubstrateOptions.JavaAgent, "")); | ||
| } | ||
|
|
||
| try { | ||
| Class<?> clazz = Class.forName(premainClass, false, cl); | ||
| Method premain = findPremainMethod(premainClass, clazz); | ||
|
|
||
| List<Object> args = new ArrayList<>(); | ||
| /* The first argument contains the premain options, which will be set at runtime. */ | ||
| args.add(""); | ||
| args.add(options); | ||
| if (premain.getParameterCount() == 2) { | ||
| args.add(new PreMainSupport.NativeImageNoOpRuntimeInstrumentation()); | ||
| } | ||
|
|
||
| support.registerPremainMethod(premainClass, premain, args.toArray(new Object[0])); | ||
| } catch (ClassNotFoundException e) { | ||
| UserError.abort("Could not register agent premain method because class %s was not found. Please check your %s setting.", premainClass, | ||
| SubstrateOptionsParser.commandArgument(Options.PremainClasses, "")); | ||
| throw UserError.abort("Could not register agent premain method because class %s was not found. Please check your %s setting.", premainClass, | ||
| SubstrateOptionsParser.commandArgument(SubstrateOptions.JavaAgent, "")); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /* | ||
| * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. | ||
| * Copyright (c) 2025, 2025, Alibaba Group Holding Limited. 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.test.javaagent; | ||
|
|
||
| /** | ||
| * Assertions used inside agent when not using JUNIT. | ||
| */ | ||
| public class AssertInAgent { | ||
| public static void assertNotNull(Object o) { | ||
| if (o == null) { | ||
| throw new RuntimeException("Object input is null, but expected to be non-null"); | ||
| } | ||
| } | ||
|
|
||
| public static void assertEquals(boolean expected, boolean actual) { | ||
| if (expected != actual) { | ||
| throw new RuntimeException(String.format("Expected(%s) is not equal to actual(%s)", expected, actual)); | ||
| } | ||
| } | ||
|
|
||
| public static void assertEquals(long expected, long actual) { | ||
| if (expected != actual) { | ||
| throw new RuntimeException(String.format("Expected(%s) is not equal to actual(%s)", expected, actual)); | ||
| } | ||
| } | ||
|
|
||
| public static void assertEquals(Object expected, Object actual) { | ||
| if (expected != actual) { | ||
| if (expected != null) { | ||
| assertNotNull(actual); | ||
| if (!expected.equals(actual)) { | ||
| throw new RuntimeException(String.format("Expected(%s) is not equal to actual(%s)", expected, actual)); | ||
| } | ||
| } else { | ||
| throw new RuntimeException(String.format("Expected(null) is not equal to actual(%s)", actual)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public static void assertTrue(boolean actual) { | ||
| assertEquals(true, actual); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the
agent3in the PR? Is it committed?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
python's
range(x, y)includes x but excludes y.