Skip to content
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

feat(attach): implement basic dynamic attach #234

Merged
merged 15 commits into from
Apr 5, 2024

Conversation

andrewazores
Copy link
Member

@andrewazores andrewazores commented Oct 16, 2023

Based on #224
Related to #233

This implements basic dynamic attach. The Agent can be launched as a standalone Java process like:

$ java -jar target/cryostat-agent-0.4.0-SNAPSHOT.jar 1234 # 1234 is another JVM's pid

This will cause the Agent process to look up PID 1234 using its Attach providers. If 1234 is found as a valid PID then the Agent process will attach to PID 1234 and attempt to load the Agent's own JAR into that running JVM, which will then bootstrap into the normal Agent launch process. If 1234 is not found then nothing is bootstrapped. Instead of 1234 the wildcard * can also be specified, which will lead to the Agent JAR attempting to bootstrap itself into every JVM it finds. Finally, if the PID is not specified or is 0, then the Agent will check if exactly one candidate JVM is available and attempt to bootstrap into that, failing if there are too many JVM candidates or none.

In this mode the Agent still tries to load its configuration entirely from the JVM it is bootstrapping into, which does not really make sense for a late-attaching Agent - the host application probably does not have Cryostat Agent-specific configuration properties if it was not launched with the Cryostat Agent to begin with. This will be addressed some time later in a follow-up PR.

Additional late-binding configuration options can be specified to the Agent launcher with command line options:

$ java -jar target/cryostat-agent-0.4.0-SNAPSHOT.jar \
-Dcryostat.agent.baseuri=http://cryostat.local \
--smartTrigger=[ProcessCpuLoad>0.2]~profile \
@/deployment/app/moreAgentArgs \
1234

Try $ java -jar target/cryostat-agent-0.4.0-SNAPSHOT.jar -h and see picocli for a more complete explanation of the available options and their behaviour. These late-binding configuration options are picked up by the Agent launcher process and serialized into a single String argument that is passed into the injected Agent instances, which they receive on their agentmain(String arg) and then deserialize again. System properties specified with -D are set onto the host JVM before the injected Agent attempts to read its configuration values, so from the Agent implementation POV this is identical to setting the -D or equivalent environment variable on the host JVM process itself. This mechanism does allow the user who controls the agent injection to override other arbitrary system properties of the host JVM at runtime, however if the user has enough access and ability to inject an agent then they can already exploit this without using the Cryostat Agent specifically.

To test:

$ ./mvnw clean install
$ gh repo clone andrewazores/quarkus-test
$ cd quarkus-test
$ git checkout agent-dynamic-attach
$ ./build.bash # build a quarkus-test image with the new agent included in the image but not launched with -javaagent
$ ./run.bash # run a quarkus-test container, then after a few seconds, spawn a Cryostat Agent process to dynamically attach to the quarkus server

Things to investigate:

  1. Dynamic attach/agent JAR loading "remotely". If the host application is running in a container that does not already contain the agent JAR, how can we get the agent injected and dynamically attached to the application in the container without having to restart the container? Dynamic attach means the agent loading doesn't require a restart, but adding a new mounted volume to the container might. The attach mechanism, at least the one used in this PR, allows for attachment to a remote VM, but requesting the VM to load the agent requires a JAR path local to the VM's filesystem. Is there some way to get the VM to load from a ByteArrayInputStream or something so we can send it the JAR contents directly?
  2. or 1b - is it feasible to rewrite parts of the agent to not actually rely on being part of the host application directly, but instead use only the local management agent (JMX)? Would also need to investigate how to ensure that's kept locked down and never exposed to any other processes, at least none outside of the container, if the agent launcher is the one turning that feature on. If this can be done then actually the agent would not need to inject itself into VMs but can run alongside them as a separate process and use local management RMI. This seems a bit hairy and is probably best avoided however. Perhaps that's best suited for another component that is not an agent, but rather a sidecar collector process, which may not implement all of the same features as the agent.
  3. Watch mode? Currently the agent launcher will execute once, attempting to inject itself into one or more VMs, and then exit. If the user specified the * PID wildcard, it might also be useful to have a --watch option where the agent launcher becomes a daemon, injecting agent instances into new JVMs as they appear.

@github-actions github-actions bot added the needs-triage Needs thorough attention from code reviewers label Oct 16, 2023
@andrewazores andrewazores added feat New feature or request and removed needs-triage Needs thorough attention from code reviewers labels Oct 16, 2023
@mergify mergify bot added the safe-to-test label Oct 16, 2023
@andrewazores andrewazores marked this pull request as ready for review October 20, 2023 19:58
@andrewazores andrewazores requested a review from a team as a code owner February 7, 2024 16:38
Copy link
Member

@ebaron ebaron left a comment

Choose a reason for hiding this comment

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

Is there a suggested way to run this after #311? As expected, when running with the plain jar, I get:

$ java -jar target/cryostat-agent-0.4.0-SNAPSHOT.jar 
Exception in thread "main" java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory
	at io.cryostat.agent.Agent.<clinit>(Agent.java:73)
Caused by: java.lang.ClassNotFoundException: org.slf4j.LoggerFactory
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
	... 1 more

With the shaded jar, I get:

$ java -jar target/cryostat-agent-0.4.0-SNAPSHOT-shaded.jar 
2024-02-26 15:59:23:538 -0500 [main] INFO io.cryostat.agent.Agent - Attaching to VM: io.cryostat.Cryostat 19841
2024-02-26 15:59:23:542 -0500 [main] INFO io.cryostat.agent.Agent - Injecting agent into PID 19841
2024-02-26 15:59:23:542 -0500 [main] INFO io.cryostat.agent.Agent - /home/ebaron/git/cryostat-agent/target/cryostat-agent-0.4.0-SNAPSHOT-shaded.jar
com.sun.tools.attach.AgentLoadException: Agent JAR not found or no Agent-Class attribute
	at jdk.attach/sun.tools.attach.HotSpotVirtualMachine.loadAgent(HotSpotVirtualMachine.java:160)
	at io.cryostat.agent.Agent.call(Agent.java:136)
	at io.cryostat.agent.Agent.call(Agent.java:63)
	at io.cryostat.agent.shaded.picocli.CommandLine.executeUserObject(CommandLine.java:2041)
	at io.cryostat.agent.shaded.picocli.CommandLine.access$1500(CommandLine.java:148)
	at io.cryostat.agent.shaded.picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2461)
	at io.cryostat.agent.shaded.picocli.CommandLine$RunLast.handle(CommandLine.java:2453)
	at io.cryostat.agent.shaded.picocli.CommandLine$RunLast.handle(CommandLine.java:2415)
	at io.cryostat.agent.shaded.picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:2273)
	at io.cryostat.agent.shaded.picocli.CommandLine$RunLast.execute(CommandLine.java:2417)
	at io.cryostat.agent.shaded.picocli.CommandLine.execute(CommandLine.java:2170)
	at io.cryostat.agent.Agent.main(Agent.java:109)

I expected the shaded jar to work. It appears to find the correct path to the shaded jar, and the manifest contains the correct Agent-Class attribute, yet the VM still fails to load it.

@andrewazores
Copy link
Member Author

Hmm, I'll have to take a look.

@andrewazores
Copy link
Member Author

$ ./run.bash 
2e0973cfc831888e7c73a099b1433dff64103f704ceaeac0ef9a583088f85cb9
Starting the Java application using /opt/jboss/container/java/run/run-java.sh ...
INFO exec  java -XX:+UseParallelGC -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20 -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:+ExitOnOutOfMemoryError -Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -Dcom.sun.management.jmxremote.port=9097 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -cp "." -jar /deployments/quarkus-run.jar 



__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
2024-02-26 21:22:50,914 INFO  [io.quarkus] (main) quarkus-test 1.0.0-SNAPSHOT on JVM (powered by Quarkus 2.7.2.Final) started in 0.427s. Listening on: http://0.0.0.0:10010
2024-02-26 21:22:50,915 INFO  [io.quarkus] (main) Profile prod activated. 
2024-02-26 21:22:50,915 INFO  [io.quarkus] (main) Installed features: [cdi, rest-client, rest-client-jackson, resteasy, smallrye-context-propagation, vertx]



2024-02-26 21:22:53:571 +0000 [main] INFO io.cryostat.agent.Agent - Attaching to VM: /deployments/quarkus-run.jar 1
2024-02-26 21:22:53:679 +0000 [main] INFO io.cryostat.agent.Agent - Injecting agent into PID 1
Agent launcher command exited: 0
2024-02-26 21:22:53:692 +0000 [cryostat-agent-main] INFO io.cryostat.agent.Agent - Cryostat Agent starting...
2024-02-26 21:22:53:692 +0000 [cryostat-agent-main] INFO io.cryostat.agent.Agent - Cryostat Agent starting...
2024-02-26 21:22:53:692 +0000 [cryostat-agent-main] INFO io.cryostat.agent.Agent - Set system property cryostat.agent.baseuri = http://localhost:9876
2024-02-26 21:22:53:841 +0000 [cryostat-agent-main] INFO io.cryostat.agent.CryostatClient - Using Cryostat baseuri http://localhost:9876
2024-02-26 21:22:53:843 +0000 [cryostat-agent-main] ERROR io.cryostat.agent.Agent - Agent startup failure
java.util.NoSuchElementException: SRCFG00040: The config property cryostat.agent.callback is defined as the empty String ("") which the following Converter considered to be null: io.cryostat.agent.shaded.io.smallrye.config.ImplicitConverters$ConstructorConverter
	at io.cryostat.agent.shaded.io.smallrye.config.SmallRyeConfig.convertValue(SmallRyeConfig.java:300)
	at io.cryostat.agent.shaded.io.smallrye.config.SmallRyeConfig.getValue(SmallRyeConfig.java:242)
	at io.cryostat.agent.shaded.io.smallrye.config.SmallRyeConfig.getValue(SmallRyeConfig.java:167)
	at io.cryostat.agent.ConfigModule.provideCryostatAgentCallback(ConfigModule.java:144)
	at io.cryostat.agent.ConfigModule_ProvideCryostatAgentCallbackFactory.provideCryostatAgentCallback(ConfigModule_ProvideCryostatAgentCallbackFactory.java:44)
	at io.cryostat.agent.ConfigModule_ProvideCryostatAgentCallbackFactory.get(ConfigModule_ProvideCryostatAgentCallbackFactory.java:35)
	at io.cryostat.agent.ConfigModule_ProvideCryostatAgentCallbackFactory.get(ConfigModule_ProvideCryostatAgentCallbackFactory.java:13)
	at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)
	at io.cryostat.agent.MainModule_ProvideRegistrationFactory.get(MainModule_ProvideRegistrationFactory.java:74)
	at io.cryostat.agent.MainModule_ProvideRegistrationFactory.get(MainModule_ProvideRegistrationFactory.java:13)
	at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)
	at io.cryostat.agent.DaggerAgent_Client$ClientImpl.registration(DaggerAgent_Client.java:238)
	at io.cryostat.agent.Agent.accept(Agent.java:226)
	at io.cryostat.agent.Agent.lambda$agentmain$0(Agent.java:159)
	at java.base/java.lang.Thread.run(Thread.java:833)


$ java -jar target/cryostat-agent-0.6.0-SNAPSHOT-shaded.jar 
java.lang.IllegalStateException: Too many available virtual machines. Auto-attach only progresses if there is one candidate. VMs: [sun.tools.attach.AttachProviderImpl@6ea12c19: 2364544 target/cryostat-agent-0.6.0-SNAPSHOT-shaded.jar, sun.tools.attach.AttachProviderImpl@6ea12c19: 2260186 org.codehaus.plexus.classworlds.launcher.Launcher, sun.tools.attach.AttachProviderImpl@6ea12c19: 1970820 /home/work/.config/coc/extensions/node_modules/coc-java/server/plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar -configuration /home/work/.config/coc/extensions/coc-java-data/1.15.2/config_linux -data /home/work/.config/coc/extensions/coc-java-data/jdt_ws_2b8f27a9c82c8ee974288c85e46ad759]
	at io.cryostat.agent.Agent.getAttachPid(Agent.java:174)
	at io.cryostat.agent.Agent.call(Agent.java:121)
	at io.cryostat.agent.Agent.call(Agent.java:63)
	at io.cryostat.agent.shaded.picocli.CommandLine.executeUserObject(CommandLine.java:2041)
	at io.cryostat.agent.shaded.picocli.CommandLine.access$1500(CommandLine.java:148)
	at io.cryostat.agent.shaded.picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2461)
	at io.cryostat.agent.shaded.picocli.CommandLine$RunLast.handle(CommandLine.java:2453)
	at io.cryostat.agent.shaded.picocli.CommandLine$RunLast.handle(CommandLine.java:2415)
	at io.cryostat.agent.shaded.picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:2273)
	at io.cryostat.agent.shaded.picocli.CommandLine$RunLast.execute(CommandLine.java:2417)
	at io.cryostat.agent.shaded.picocli.CommandLine.execute(CommandLine.java:2170)
	at io.cryostat.agent.Agent.main(Agent.java:109)

$ java -jar target/cryostat-agent-0.6.0-SNAPSHOT-shaded.jar 2364683
2024-02-26 16:25:23:188 -0500 [main] INFO io.cryostat.agent.Agent - Attaching to VM: /usr/lib/jmc//plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar -os linux -ws gtk -arch x86_64 -showsplash -launcher /usr/lib/jmc/jmc -name Jmc --launcher.library /usr/lib/jmc//plugins/org.eclipse.equinox.launcher.gtk.linux.x86_64_1.2.500.v20220509-0833/eclipse_11701.so -startup /usr/lib/jmc//plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar --launcher.appendVmargs -exitdata 7800d -vm /usr/bin/java -vmargs -XX:+IgnoreUnrecognizedVMOptions -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -XX:FlightRecorderOptions=stackdepth=128 -XX:+FlightRecorder -XX:StartFlightRecording=name=JMC_Default,maxsize=100m -Djava.net.preferIPv4Stack=true -Djdk.attach.allowAttachSelf=true --add-exports=java.xml/com.sun.org.apache.xerces.internal.parsers=ALL-UNNAMED --add-exports=jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED --add-exports=java.management/sun.management=ALL-UNNAMED --add-exports=java.management/sun.management.counter.perf=ALL-UNNAMED --add-exports=jdk.management.agent/jdk.inter 2364683
2024-02-26 16:25:23:194 -0500 [main] INFO io.cryostat.agent.Agent - Injecting agent into PID 2364683

and

$ jmc 
OpenJDK 64-Bit Server VM warning: Option FlightRecorder was deprecated in version 13.0 and will likely be removed in a future release.
[0.176s][info][jfr,startup] Started recording 1.
[0.176s][info][jfr,startup] 
[0.176s][info][jfr,startup] Use jcmd 2364683 JFR.dump name=JMC_Default filename=FILEPATH to copy recording data to file.
2024-02-26 16:25:23:204 -0500 [cryostat-agent-main] INFO io.cryostat.agent.Agent - Cryostat Agent starting...
2024-02-26 16:25:23:205 -0500 [cryostat-agent-main] INFO io.cryostat.agent.Agent - Cryostat Agent starting...
2024-02-26 16:25:23:254 -0500 [cryostat-agent-main] ERROR io.cryostat.agent.Agent - Agent startup failure
java.util.NoSuchElementException: SRCFG00040: The config property cryostat.agent.baseuri is defined as the empty String ("") which the following Converter considered to be null: io.cryostat.agent.shaded.io.smallrye.config.ImplicitConverters$ConstructorConverter
	at io.cryostat.agent.shaded.io.smallrye.config.SmallRyeConfig.convertValue(SmallRyeConfig.java:300)
	at io.cryostat.agent.shaded.io.smallrye.config.SmallRyeConfig.getValue(SmallRyeConfig.java:242)
	at io.cryostat.agent.shaded.io.smallrye.config.SmallRyeConfig.getValue(SmallRyeConfig.java:167)
	at io.cryostat.agent.ConfigModule.provideCryostatAgentBaseUri(ConfigModule.java:137)
	at io.cryostat.agent.ConfigModule_ProvideCryostatAgentBaseUriFactory.provideCryostatAgentBaseUri(ConfigModule_ProvideCryostatAgentBaseUriFactory.java:44)
	at io.cryostat.agent.ConfigModule_ProvideCryostatAgentBaseUriFactory.get(ConfigModule_ProvideCryostatAgentBaseUriFactory.java:35)
	at io.cryostat.agent.ConfigModule_ProvideCryostatAgentBaseUriFactory.get(ConfigModule_ProvideCryostatAgentBaseUriFactory.java:13)
	at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)
	at io.cryostat.agent.DaggerAgent_Client$ClientImpl.baseUri(DaggerAgent_Client.java:223)
	at io.cryostat.agent.Agent.accept(Agent.java:216)
	at io.cryostat.agent.Agent.lambda$agentmain$0(Agent.java:159)
	at java.base/java.lang.Thread.run(Thread.java:840)

Not sure what's going on - from my end it looks like it's just about working as intended. I haven't tried supplying the injected Agent with its required configuration options yet, but it's at least getting to that point.

Copy link
Member

@ebaron ebaron left a comment

Choose a reason for hiding this comment

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

I can reproduce a working setup with those instructions. Sorry for the delay (again)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
Status: Done
Development

Successfully merging this pull request may close these issues.

2 participants