Skip to content

Commit f3bd8dd

Browse files
committed
Implement better jattach support and bump version
1 parent 04019c7 commit f3bd8dd

File tree

3 files changed

+164
-10
lines changed

3 files changed

+164
-10
lines changed

Diff for: README.md

+29-7
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ Loader for AsyncProfiler
66
Packages [async-profiler](https://github.com/jvm-profiling-tools/async-profiler) releases in a JAR
77
with an `AsyncProfilerLoader` (version 2.* and 1.8.*) that loads the suitable native library for the current platform.
88

9-
This is usable as a java agent (same arguments as the async-profiler agent) and as the basis for other libraries.
9+
This is usable as a Java agent (same arguments as the async-profiler agent) and as the basis for other libraries.
1010
The real rationale behind this library is that the async-profiler is a nice tool, but it cannot be easily integrated
1111
into other Java-based tools.
1212

13+
The `AsyncProfilerLoader` API integrates async-profiler and jattach with a user-friendly interface (see below).
14+
1315
The wrapper is tested against all relevant tests of the async-profiler tool, ensuring that it has the same behavior.
1416

1517
Take the [`all` build](https://github.com/jvm-profiling-tools/ap-loader/releases/latest/download/ap-loader-all.jar) and you have a JAR that provides the important features of async-profiler on all supported
@@ -42,7 +44,7 @@ Or you can depend on the artifacts from maven central, they should be slightly m
4244
<dependency>
4345
<groupId>me.bechberger</groupId>
4446
<artifactId>ap-loader-all</artifactId>
45-
<version>2.9-4</version>
47+
<version>2.9-5</version>
4648
</dependency>
4749
```
4850

@@ -197,6 +199,19 @@ The API of the `AsyncProfilerLoader` can be used to execute all commands of the
197199
198200
The converters reside in the `one.converter` package.
199201
202+
Attaching a Custom Agent Programmatically
203+
---------------------------------
204+
A notable part of the API are the jattach related methods that allow you to call `jattach` to attach
205+
your own native library to the currently running JVM:
206+
207+
```java
208+
// extract the agent first from the resources
209+
Path p = one.profiler.AsyncProfilerLoader.extractCustomLibraryFromResources(....getClassLoader(), "library name");
210+
// attach the agent to the current JVM
211+
one.profiler.AsyncProfilerLoader.jattach(p, "optional arguments")
212+
// -> returns true if jattach succeeded
213+
```
214+
200215
### Releases
201216
202217
```xml
@@ -213,7 +228,7 @@ The latest `all` version can be added via:
213228
<dependency>
214229
<groupId>me.bechberger</groupId>
215230
<artifactId>ap-loader-all</artifactId>
216-
<version>2.9-4</version>
231+
<version>2.9-5</version>
217232
</dependency>
218233
```
219234
@@ -233,7 +248,7 @@ For example for the `all` variant of version 2.9:
233248
<dependency>
234249
<groupId>me.bechberger</groupId>
235250
<artifactId>ap-loader-all</artifactId>
236-
<version>2.9-4-SNAPSHOT</version>
251+
<version>2.9-5-SNAPSHOT</version>
237252
</dependency>
238253
```
239254
@@ -270,11 +285,11 @@ python3 ./bin/releaser.py download 2.9
270285
# build the JAR for the release
271286
# maven might throw warnings, related to the project version setting,
272287
# but the alternative solutions don't work, so we ignore the warning for now
273-
mvn -Dproject.vversion=2.9 -Dproject.subrelease=4 -Dproject.platform=macos package assembly:single
288+
mvn -Dproject.vversion=2.9 -Dproject.subrelease=5 -Dproject.platform=macos package assembly:single
274289
# use it
275-
java -jar target/ap-loader-macos-2.9-4-full.jar ...
290+
java -jar target/ap-loader-macos-2.9-5-full.jar ...
276291
# build the all JAR
277-
mvn -Dproject.vversion=2.9 -Dproject.subrelease=4 -Dproject.platform=all package assembly:single
292+
mvn -Dproject.vversion=2.9 -Dproject.subrelease=5 -Dproject.platform=all package assembly:single
278293
```
279294
280295
Development
@@ -319,6 +334,13 @@ And the following for a new async-profiler release:
319334
Changelog
320335
---------
321336
337+
### v5
338+
339+
- Add new jattach methods (`AsyncProfilerLoader.jattach(Path agent, String args)`) to make using it programmatically easier
340+
- Add new `AsyncProfilerLoader.extractCustomLibraryFromResources(ClassLoader, String)`
341+
method to extract a custom library from the resources
342+
- this also has a variant that looks in an alternative resource directory if the resource does not exist
343+
322344
### v4
323345
324346
- `AsyncProfiler.isSupported()` now returns `false` if the OS is not supported by any async-profiler binary, fixes #5

Diff for: bin/releaser.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
from typing import Any, Dict, List, Union, Tuple, Optional
1818
from urllib import request
1919

20-
SUB_VERSION = 4
21-
RELEASE_NOTES = """- `AsyncProfiler.isSupported()` now returns `false` if the OS is not supported by any async-profiler binary, fixes #5"""
20+
SUB_VERSION = 5
21+
RELEASE_NOTES = """- Add new jattach methods (`AsyncProfilerLoader.jattach(Path agent, String args)`) to make using it programmatically easier
22+
- Add new `AsyncProfilerLoader.extractCustomLibraryFromResources(ClassLoader, String)`
23+
method to extract a custom library from the resources
24+
- this also has a variant that looks in an alternative resource directory if the resource does not exist
25+
"""
2226

2327
HELP = """
2428
Usage:

Diff for: src/main/java/one/profiler/AsyncProfilerLoader.java

+129-1
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,86 @@ private static Path copyFromResources(String fileName, Path destination) throws
299299
}
300300
}
301301

302+
/**
303+
* Extracts a custom agent from the resources
304+
*
305+
* <p>
306+
*
307+
* @param classLoader the class loader to load the resources from
308+
* @param fileName the name of the file to copy, maps the library name if the fileName does not start with "lib",
309+
* e.g. "jni" will be treated as "libjni.so" on Linux and as "libjni.dylib" on macOS
310+
* @return the path of the library
311+
* @throws IOException if the extraction fails
312+
*/
313+
public static Path extractCustomLibraryFromResources(ClassLoader classLoader, String fileName) throws IOException {
314+
return extractCustomLibraryFromResources(classLoader, fileName, null);
315+
}
316+
317+
/**
318+
* Extracts a custom native library from the resources and returns the alternative source
319+
* if the file is not in the resources.
320+
*
321+
* <p>If the file is extracted, then it is copied to a new temporary folder which is deleted upon JVM exit.</p>
322+
*
323+
* <p>This method is mainly seen as a helper method to obtain custom native agents for {@link #jattach(Path)} and
324+
* {@link #jattach(Path, String)}. It is included in ap-loader to make it easier to write applications that need
325+
* custom native libraries.</p>
326+
*
327+
* <p>This method works on all architectures.</p>
328+
*
329+
* @param classLoader the class loader to load the resources from
330+
* @param fileName the name of the file to copy, maps the library name if the fileName does not start with "lib",
331+
* e.g. "jni" will be treated as "libjni.so" on Linux and as "libjni.dylib" on macOS
332+
* @param alternativeSource the optional resource directory to use if the resource is not found in the resources,
333+
* this is typically the case when running the application from an IDE, an example would be
334+
* "src/main/resources" or "target/classes" for maven projects
335+
* @return the path of the library
336+
* @throws IOException if the extraction fails and the alternative source is not present for the current architecture
337+
*/
338+
public static Path extractCustomLibraryFromResources(ClassLoader classLoader, String fileName, Path alternativeSource) throws IOException {
339+
Path filePath = Paths.get(fileName);
340+
String name = filePath.getFileName().toString();
341+
if (!name.startsWith("lib")) {
342+
name = System.mapLibraryName(name);
343+
}
344+
Path realFilePath = filePath.getParent() == null ? Paths.get(name) : filePath.getParent().resolve(name);
345+
Enumeration<URL> indexFiles = classLoader.getResources(realFilePath.toString());
346+
if (!indexFiles.hasMoreElements()) {
347+
if (alternativeSource == null) {
348+
throw new IOException("Could not find library " + fileName + " in resources");
349+
}
350+
if (!alternativeSource.toFile().isDirectory()) {
351+
throw new IOException("Could not find library " + fileName + " in resources and alternative source " + alternativeSource + " is not a directory");
352+
}
353+
if (alternativeSource.resolve(realFilePath).toFile().exists()) {
354+
return alternativeSource.resolve(realFilePath);
355+
}
356+
throw new IOException("Could not find library " + fileName + " in resources and alternative source " + alternativeSource + " does not contain " + realFilePath);
357+
}
358+
URL url = indexFiles.nextElement();
359+
Path tempDir = Files.createTempDirectory("ap-loader");
360+
try {
361+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
362+
try (Stream<Path> stream = Files.walk(getExtractionDirectory())) {
363+
stream.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
364+
} catch (IOException ex) {
365+
throw new RuntimeException(ex);
366+
}
367+
}));
368+
} catch (RuntimeException e) {
369+
throw (IOException) e.getCause();
370+
}
371+
Path destination = tempDir.resolve(name);
372+
try {
373+
try (InputStream in = url.openStream()) {
374+
Files.copy(in, destination);
375+
}
376+
return destination;
377+
} catch (IOException e) {
378+
throw new IOException("Could not copy file " + fileName + " to " + destination, e);
379+
}
380+
}
381+
302382
/**
303383
* Extracts the jattach tool
304384
*
@@ -405,6 +485,9 @@ private static String[] processJattachArgs(String[] args) throws IOException {
405485
* One can therefore start/stop the async-profiler via <code>
406486
* executeJattach(PID, "load", "libasyncProfiler.so", true, "start"/"stop")</code>.
407487
*
488+
* Use the {@link #jattach(Path)} or {@link #jattach(Path, String)} to load agents via jattach directly,
489+
* without the need to construct the command line arguments yourself.
490+
*
408491
* @throws IOException if something went wrong (e.g. the jattach binary is not found or the
409492
* execution fails)
410493
* @throws IllegalStateException if OS or Arch are not supported
@@ -417,6 +500,46 @@ private static void executeJattachInteractively(String[] args) throws IOExceptio
417500
executeCommandInteractively("jattach", processJattachArgs(args));
418501
}
419502

503+
/**
504+
* See <a href="https://github.com/apangin/jattach">jattach</a> for more information.
505+
*
506+
* <p>It loads the passed agent via jattach to the current JVM, mapping
507+
* "libasyncProfiler.so" to the extracted async-profiler library for the load command.</p>
508+
*
509+
* @return true if the agent was successfully attached, false otherwise
510+
* @throws IllegalStateException if OS or Arch are not supported
511+
*/
512+
public static boolean jattach(Path agentPath) {
513+
return jattach(agentPath, null);
514+
}
515+
516+
/**
517+
* See <a href="https://github.com/apangin/jattach">jattach</a> for more information.
518+
*
519+
* <p>It loads the passed agent via jattach to the current JVM, mapping
520+
* "libasyncProfiler.so" to the extracted async-profiler library for the load command.</p>
521+
*
522+
* @return true if the agent was successfully attached, false otherwise
523+
* @throws IllegalStateException if OS or Arch are not supported
524+
*/
525+
public static boolean jattach(Path agentPath, String arguments) {
526+
List<String> args = new ArrayList<>();
527+
args.add(String.valueOf(getProcessId()));
528+
args.add("load");
529+
args.add(agentPath.toString());
530+
args.add("true");
531+
if (arguments != null) {
532+
args.add(arguments);
533+
}
534+
try {
535+
executeJattach(args.toArray(new String[0]));
536+
return true;
537+
} catch (IOException e) {
538+
e.printStackTrace();
539+
return false;
540+
}
541+
}
542+
420543
private static String[] processConverterArgs(String[] args) throws IOException {
421544
List<String> argList = new ArrayList<>();
422545
argList.add(System.getProperty("java.home") + "/bin/java");
@@ -597,7 +720,12 @@ public static void premain(String agentArgs, Instrumentation instrumentation) {
597720
agentmain(agentArgs, instrumentation);
598721
}
599722

600-
private static int getProcessId() {
723+
/**
724+
* Returns the id of the current process
725+
*
726+
* @throws IllegalStateException if the id can not be obtained, this should never happen
727+
*/
728+
public static int getProcessId() {
601729
String name = ManagementFactory.getRuntimeMXBean().getName();
602730
int index = name.indexOf('@');
603731
if (index < 1) {

0 commit comments

Comments
 (0)